Skip to content

Commit a938fcb

Browse files
committed
Replace deprecated license metadata in templates
Switch cookiecutter license to SPDX expressions, add license-files, and remove deprecated classifiers. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
1 parent 17a463b commit a938fcb

8 files changed

Lines changed: 141 additions & 17 deletions

File tree

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Summary
44

55
This release migrates lightweight GitHub Actions workflow jobs to use the new cost-effective `ubuntu-slim` runner.
6+
It also updates cookiecutter pyproject license metadata to SPDX expressions to avoid setuptools deprecation warnings.
67

78
## Upgrading
89

@@ -41,4 +42,5 @@ But you might still need to adapt your code:
4142

4243
### Cookiecutter template
4344

44-
<!-- Here bug fixes for cookiecutter specifically -->
45+
- Switched `project.license` to SPDX expressions and added `project.license-files`.
46+
This removes deprecated setuptools license metadata and avoids build warnings.

cookiecutter/migrate.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import hashlib
2424
import json
2525
import os
26+
import re
2627
import subprocess
2728
import tempfile
2829
from pathlib import Path
@@ -36,6 +37,9 @@ def main() -> None:
3637
print("Migrating workflows to use ubuntu-slim runner for lightweight jobs...")
3738
migrate_to_ubuntu_slim()
3839
print("=" * 72)
40+
print("Migrating pyproject license metadata to SPDX format...")
41+
migrate_pyproject_license()
42+
print("=" * 72)
3943
print("Migration script finished. Remember to follow any manual instructions.")
4044
print("=" * 72)
4145

@@ -198,6 +202,81 @@ def migrate_to_ubuntu_slim() -> None:
198202
)
199203

200204

205+
def migrate_pyproject_license() -> None: # pylint: disable=too-many-branches
206+
"""Migrate pyproject license metadata to SPDX expressions."""
207+
pyproject_path = Path("pyproject.toml")
208+
if not pyproject_path.exists():
209+
print(" Skipping pyproject.toml (file not found)")
210+
return
211+
212+
content = pyproject_path.read_text(encoding="utf-8")
213+
new_content = content
214+
updated = False
215+
216+
license_expression = None
217+
for old_license, new_license in (
218+
("MIT", "MIT"),
219+
("Proprietary", "LicenseRef-Proprietary"),
220+
("Propietary", "LicenseRef-Proprietary"),
221+
):
222+
old_line = f'license = {{ text = "{old_license}" }}'
223+
if old_line in new_content:
224+
new_content = new_content.replace(old_line, f'license = "{new_license}"', 1)
225+
license_expression = new_license
226+
updated = True
227+
break
228+
229+
if license_expression is None:
230+
for existing_license in ("MIT", "LicenseRef-Proprietary"):
231+
if f'license = "{existing_license}"' in new_content:
232+
license_expression = existing_license
233+
break
234+
235+
if license_expression is None:
236+
cookiecutter_license = read_cookiecutter_license()
237+
if cookiecutter_license == "MIT":
238+
license_expression = "MIT"
239+
elif cookiecutter_license == "Proprietary":
240+
license_expression = "LicenseRef-Proprietary"
241+
242+
if license_expression is None:
243+
manual_step(
244+
"Unable to detect project license in pyproject.toml. Please set "
245+
"`project.license` to a SPDX expression and add "
246+
'`project.license-files = ["LICENSE"]`.'
247+
)
248+
return
249+
250+
license_line = f'license = "{license_expression}"'
251+
if "license-files" not in new_content and license_line in new_content:
252+
new_content = new_content.replace(
253+
license_line, f'{license_line}\nlicense-files = ["LICENSE"]', 1
254+
)
255+
updated = True
256+
257+
for classifier in (
258+
"License :: OSI Approved :: MIT License",
259+
"License :: Other/Proprietary License",
260+
):
261+
classifier_line = f' "{classifier}",\n'
262+
if classifier_line in new_content:
263+
new_content = new_content.replace(classifier_line, "", 1)
264+
updated = True
265+
266+
setuptools_version = parse_setuptools_version(new_content)
267+
if setuptools_version is not None and setuptools_version < 77:
268+
new_content, replaced = replace_setuptools_pin(new_content, "80.9.0")
269+
if replaced:
270+
updated = True
271+
272+
if not updated or new_content == content:
273+
print(" Skipped pyproject.toml (already up to date)")
274+
return
275+
276+
replace_file_contents_atomically(pyproject_path, content, new_content, count=1)
277+
print(" Updated pyproject.toml: migrated license metadata")
278+
279+
201280
def read_project_type() -> str | None:
202281
"""Read the cookiecutter project type from the replay file."""
203282
replay_path = Path(".cookiecutter-replay.json")
@@ -220,6 +299,47 @@ def read_project_type() -> str | None:
220299
return project_type
221300

222301

302+
def read_cookiecutter_license() -> str | None:
303+
"""Read the cookiecutter license from the replay file."""
304+
replay_path = Path(".cookiecutter-replay.json")
305+
if not replay_path.exists():
306+
return None
307+
308+
try:
309+
data = json.loads(replay_path.read_text(encoding="utf-8"))
310+
except (json.JSONDecodeError, OSError):
311+
return None
312+
313+
cookiecutter_data = data.get("cookiecutter")
314+
if not isinstance(cookiecutter_data, dict):
315+
return None
316+
317+
license_value = cookiecutter_data.get("license")
318+
if not isinstance(license_value, str):
319+
return None
320+
321+
return license_value
322+
323+
324+
def parse_setuptools_version(content: str) -> int | None:
325+
"""Parse the setuptools major version from pyproject content."""
326+
match = re.search(r'"setuptools\s*==\s*([0-9]+)(?:\.[0-9]+)*"', content)
327+
if not match:
328+
return None
329+
return int(match.group(1))
330+
331+
332+
def replace_setuptools_pin(content: str, new_version: str) -> tuple[str, bool]:
333+
"""Replace the setuptools pin with a new version."""
334+
new_content, count = re.subn(
335+
r'("setuptools\s*==\s*)[0-9]+(?:\.[0-9]+)*("\s*,?)',
336+
rf"\1{new_version}\2",
337+
content,
338+
count=1,
339+
)
340+
return new_content, count > 0
341+
342+
223343
def apply_patch(patch_content: str) -> None:
224344
"""Apply a patch using the patch utility."""
225345
subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True)

cookiecutter/{{cookiecutter.github_repo_name}}/pyproject.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,19 @@ build-backend = "setuptools.build_meta"
2222
name = "{{cookiecutter.pypi_package_name}}"
2323
description = "{{cookiecutter.description}}"
2424
readme = "README.md"
25-
license = { text = "{{cookiecutter.license}}" }
25+
{%- if cookiecutter.license == "MIT" %}
26+
license = "MIT"
27+
{%- elif cookiecutter.license == "Proprietary" %}
28+
license = "LicenseRef-Proprietary"
29+
{%- else %}
30+
license = "{{cookiecutter.license}}"
31+
{%- endif %}
32+
license-files = ["LICENSE"]
2633
keywords = {{cookiecutter | keywords}}
2734
# TODO(cookiecutter): Remove and add more classifiers if appropriate
2835
classifiers = [
2936
"Development Status :: 3 - Alpha",
3037
"Intended Audience :: Developers",
31-
{%- if cookiecutter.license == "MIT" %}
32-
"License :: OSI Approved :: MIT License",
33-
{%- elif cookiecutter.license == "Propietary" %}
34-
"License :: Other/Proprietary License",
35-
{%- endif %}
3638
"Programming Language :: Python :: 3",
3739
"Programming Language :: Python :: 3 :: Only",
3840
{%- if cookiecutter.type != "app" %}

tests_golden/integration/test_cookiecutter_generation/actor/frequenz-actor-test/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ build-backend = "setuptools.build_meta"
1313
name = "frequenz-actor-test"
1414
description = "Test description"
1515
readme = "README.md"
16-
license = { text = "MIT" }
16+
license = "MIT"
17+
license-files = ["LICENSE"]
1718
keywords = ["frequenz", "python", "actor", "test"]
1819
# TODO(cookiecutter): Remove and add more classifiers if appropriate
1920
classifiers = [
2021
"Development Status :: 3 - Alpha",
2122
"Intended Audience :: Developers",
22-
"License :: OSI Approved :: MIT License",
2323
"Programming Language :: Python :: 3",
2424
"Programming Language :: Python :: 3 :: Only",
2525
"Topic :: Software Development :: Libraries",

tests_golden/integration/test_cookiecutter_generation/api/frequenz-api-test/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ build-backend = "setuptools.build_meta"
2020
name = "frequenz-api-test"
2121
description = "Test description"
2222
readme = "README.md"
23-
license = { text = "MIT" }
23+
license = "MIT"
24+
license-files = ["LICENSE"]
2425
keywords = ["frequenz", "python", "api", "grpc", "protobuf", "rpc", "test"]
2526
# TODO(cookiecutter): Remove and add more classifiers if appropriate
2627
classifiers = [
2728
"Development Status :: 3 - Alpha",
2829
"Intended Audience :: Developers",
29-
"License :: OSI Approved :: MIT License",
3030
"Programming Language :: Python :: 3",
3131
"Programming Language :: Python :: 3 :: Only",
3232
"Topic :: Software Development :: Libraries",

tests_golden/integration/test_cookiecutter_generation/app/frequenz-app-test/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ build-backend = "setuptools.build_meta"
1313
name = "frequenz-app-test"
1414
description = "Test description"
1515
readme = "README.md"
16-
license = { text = "MIT" }
16+
license = "MIT"
17+
license-files = ["LICENSE"]
1718
keywords = ["frequenz", "python", "app", "application", "test"]
1819
# TODO(cookiecutter): Remove and add more classifiers if appropriate
1920
classifiers = [
2021
"Development Status :: 3 - Alpha",
2122
"Intended Audience :: Developers",
22-
"License :: OSI Approved :: MIT License",
2323
"Programming Language :: Python :: 3",
2424
"Programming Language :: Python :: 3 :: Only",
2525
"Typing :: Typed",

tests_golden/integration/test_cookiecutter_generation/lib/frequenz-test-python/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ build-backend = "setuptools.build_meta"
1313
name = "frequenz-test"
1414
description = "Test description"
1515
readme = "README.md"
16-
license = { text = "MIT" }
16+
license = "MIT"
17+
license-files = ["LICENSE"]
1718
keywords = ["frequenz", "python", "lib", "library", "test"]
1819
# TODO(cookiecutter): Remove and add more classifiers if appropriate
1920
classifiers = [
2021
"Development Status :: 3 - Alpha",
2122
"Intended Audience :: Developers",
22-
"License :: OSI Approved :: MIT License",
2323
"Programming Language :: Python :: 3",
2424
"Programming Language :: Python :: 3 :: Only",
2525
"Topic :: Software Development :: Libraries",

tests_golden/integration/test_cookiecutter_generation/model/frequenz-model-test/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ build-backend = "setuptools.build_meta"
1313
name = "frequenz-model-test"
1414
description = "Test description"
1515
readme = "README.md"
16-
license = { text = "MIT" }
16+
license = "MIT"
17+
license-files = ["LICENSE"]
1718
keywords = ["frequenz", "python", "model", "ai", "ml", "machine-learning", "test"]
1819
# TODO(cookiecutter): Remove and add more classifiers if appropriate
1920
classifiers = [
2021
"Development Status :: 3 - Alpha",
2122
"Intended Audience :: Developers",
22-
"License :: OSI Approved :: MIT License",
2323
"Programming Language :: Python :: 3",
2424
"Programming Language :: Python :: 3 :: Only",
2525
"Topic :: Software Development :: Libraries",

0 commit comments

Comments
 (0)