Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 56 additions & 28 deletions src/fosslight_source/run_manifest_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,41 +178,69 @@ def get_licenses_from_pyproject_toml(file_path: str) -> list[str]:
data = None

if isinstance(data, dict):
project_tbl = data.get('project') or {}
license_value = project_tbl.get('license')
if isinstance(license_value, str) and license_value.strip():
return [license_value.strip()]
if isinstance(license_value, dict):
text_value = license_value.get('text')
if isinstance(text_value, str) and text_value.strip():
return [text_value.strip()]
if license_value.get('file'):
return []
project_tbl = data.get('project')
if isinstance(project_tbl, dict) and 'license' in project_tbl:
license_value = project_tbl.get('license')
if isinstance(license_value, str) and license_value.strip():
return [license_value.strip()]
if isinstance(license_value, dict):
text_value = license_value.get('text')
if isinstance(text_value, str) and text_value.strip():
return [text_value.strip()]
if license_value.get('file'):
return []
return []

tool_tbl = data.get('tool')
if isinstance(tool_tbl, dict):
poetry_tbl = tool_tbl.get('poetry')
if isinstance(poetry_tbl, dict):
poetry_license = poetry_tbl.get('license')
if isinstance(poetry_license, str) and poetry_license.strip():
return [poetry_license.strip()]
# Structured parse succeeded; do not re-read/regex the same file.
return []
except Exception as ex:
logger.info(f"Failed to parse pyproject.toml via toml parser for {file_path}: {ex}")

try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
project_match = re.search(r'^\s*\[project\]\s*(.*?)(?=^\s*\[|\Z)', content, flags=re.MULTILINE | re.DOTALL)
if not project_match:
return []
block = project_match.group(1)
m = re.search(r'^\s*license\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)', block,
flags=re.MULTILINE | re.DOTALL)
if m:
val = m.group('val').strip()
if val:
return [val]
m2 = re.search(r'^\s*license\s*=\s*\{[^}]*?\btext\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)',
block, flags=re.MULTILINE | re.DOTALL)
if m2:
val = m2.group('val').strip()
if val:
return [val]
m3 = re.search(r'^\s*license\s*=\s*\{[^}]*?\bfile\s*=', block, flags=re.MULTILINE | re.DOTALL)
if m3:
return []
if project_match:
block = project_match.group(1)
project_license_declared = re.search(r'^\s*license\s*=', block, flags=re.MULTILINE)
if project_license_declared:
m = re.search(r'^\s*license\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)', block,
flags=re.MULTILINE | re.DOTALL)
if m:
val = m.group('val').strip()
if val:
return [val]
m2 = re.search(r'^\s*license\s*=\s*\{[^}]*?\btext\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)',
block, flags=re.MULTILINE | re.DOTALL)
if m2:
val = m2.group('val').strip()
if val:
return [val]
return []

poetry_match = re.search(
r'^\s*\[tool\.poetry\]\s*(.*?)(?=^\s*\[|\Z)',
content,
flags=re.MULTILINE | re.DOTALL,
)
if poetry_match:
poetry_block = poetry_match.group(1)
poetry_license = re.search(
r'^\s*license\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)',
poetry_block,
flags=re.MULTILINE | re.DOTALL,
)
if poetry_license:
val = poetry_license.group('val').strip()
if val:
return [val]
except Exception as ex:
logger.info(f"Failed to parse pyproject.toml {file_path}: {ex}")
return []
Expand Down
51 changes: 51 additions & 0 deletions tests/test_manifest_pyproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright (c) 2026 LG Electronics Inc.
# SPDX-License-Identifier: Apache-2.0
"""Tests for pyproject.toml manifest license extraction."""

from fosslight_source.run_manifest_extractor import get_licenses_from_pyproject_toml


def test_pep621_license_string(tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[project]\nname = "demo"\nlicense = "MIT"\n',
encoding="utf-8",
)
assert get_licenses_from_pyproject_toml(str(path)) == ["MIT"]


def test_pep621_license_text_table(tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[project]\nname = "demo"\nlicense = {text = "MIT"}\n',
encoding="utf-8",
)
assert get_licenses_from_pyproject_toml(str(path)) == ["MIT"]


def test_pep621_license_file_returns_empty(tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[project]\nname = "demo"\nlicense = {file = "LICENSE"}\n',
encoding="utf-8",
)
assert get_licenses_from_pyproject_toml(str(path)) == []


def test_legacy_poetry_license(tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[tool.poetry]\nname = "demo"\nlicense = "Apache-2.0"\n',
encoding="utf-8",
)
assert get_licenses_from_pyproject_toml(str(path)) == ["Apache-2.0"]


def test_project_license_file_does_not_fall_back_to_poetry(tmp_path):
path = tmp_path / "pyproject.toml"
path.write_text(
'[project]\nname = "demo"\nlicense = {file = "LICENSE"}\n\n'
'[tool.poetry]\nname = "demo"\nlicense = "MIT"\n',
encoding="utf-8",
)
assert get_licenses_from_pyproject_toml(str(path)) == []
Loading