Skip to content

Commit ddd2498

Browse files
committed
Support custom LICENSE option & new default license Apache-2.0
1 parent 325b0b4 commit ddd2498

7 files changed

Lines changed: 115 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0
1717

1818
[Full changelog](https://github.com/linkml/linkml-project-copier/compare/v0.4.1...main)
1919

20+
### Changed
21+
22+
- Default `license` is now `Apache-2.0` (was `MIT`). #151
23+
24+
### Added
25+
26+
- New `existing_license_file` prompt: when set to the path of a license file
27+
already present in the project (e.g. `LICENSE.md`, `LICENSE.txt`), the
28+
template skips generating a new `LICENSE` and points `license-files` in
29+
`pyproject.toml` at the provided file. The `license` prompt then defaults to
30+
`Custom` (recorded as the PEP 639 SPDX identifier `LicenseRef-Custom`),
31+
signalling "see the license file"; the user can override with a specific
32+
SPDX identifier if they know it. #151
33+
2034
### Fixed
2135

2236
- Fix `uv tool` command that includes jinja2-time into copier. #90

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ The `--trust` option is needed because the template uses the jinja_extension `ji
7575
You will be prompted a few questions.
7676
The defaults are fine for most projects, but pick the name for your project carefully as it will also be used as project name on GitHub.
7777

78+
If you are applying the template to an existing project that already has a license file (e.g. `LICENSE.md`, `LICENSE.txt`), answer the `existing_license_file` prompt with its path.
79+
The template will then leave your file untouched, skip generating a new `LICENSE`, and point `license-files` in `pyproject.toml` at the file you named.
80+
The `license` prompt will default to `Custom` in that case, recorded as the PEP 639 identifier `LicenseRef-Custom`; override it if you know the file's actual SPDX license.
81+
The default license for new projects is `Apache-2.0`.
82+
7883
It is also possible to use non-default branches or specific tags via `--vcs-ref` which is useful when developing the template:
7984

8085
```shell

copier.yaml

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,31 @@ project_description:
4646
type: str
4747
default: This is the project description.
4848

49+
existing_license_file:
50+
help: |
51+
If your project already has a license file, give its path relative to the
52+
project root (e.g. LICENSE.md, LICENSE.txt). Leave empty to generate a new
53+
LICENSE file from the license chosen below. The SPDX identifier is
54+
recorded in pyproject.toml either way.
55+
type: str
56+
default: ""
57+
4958
license:
50-
help: Which license do you want for your project?
59+
help: |
60+
Which license do you want for your project? Pick "Custom" if you supplied an
61+
existing license file above whose content does not match any of the listed
62+
SPDX licenses; that records the PEP 639 identifier `LicenseRef-Custom` in
63+
pyproject.toml, signalling "see the license file".
5164
type: str
5265
choices:
53-
- MIT
54-
- BSD-3-Clause
55-
- Apache-2.0
56-
- MPL-2.0
57-
- LGPL-3.0-only
58-
- GPL-3.0-only
59-
default: MIT
66+
MIT: MIT
67+
BSD-3-Clause: BSD-3-Clause
68+
Apache-2.0: Apache-2.0
69+
MPL-2.0: MPL-2.0
70+
LGPL-3.0-only: LGPL-3.0-only
71+
GPL-3.0-only: GPL-3.0-only
72+
Custom: LicenseRef-Custom
73+
default: "{{ 'LicenseRef-Custom' if existing_license_file else 'Apache-2.0' }}"
6074

6175
copyright_year:
6276
help: The year of the first release

template/pyproject.toml.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ authors = [
99
{name = "{{full_name}}", email = "{{email}}"},
1010
]
1111
license = "{{license}}"
12-
license-files = ["LICENSE"]
12+
license-files = ["{{ existing_license_file or 'LICENSE' }}"]
1313
readme = "README.md"
1414
requires-python = ">=3.10,<4.0"
1515
dynamic = ["version"]

template/LICENSE.jinja renamed to template/{% if not existing_license_file %}LICENSE{% endif %}.jinja

File renamed without changes.

tests/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"full_name": "Test User",
2020
"github_org": "test-org",
2121
"project_description": "A test project.",
22+
"existing_license_file": "",
2223
"license": "MIT",
2324
"copyright_year": "2025",
2425
"add_example": True,

tests/test_licenses.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
import sys
66

7+
from copier import run_copy
8+
9+
from tests.helpers import DEFAULT_DATA, TEMPLATE_ROOT
10+
711
if sys.version_info >= (3, 11):
812
import tomllib
913
else:
@@ -41,3 +45,71 @@ def test_pyproject_license_field(self, license_project):
4145
(project_path / "pyproject.toml").read_text(encoding="utf-8")
4246
)
4347
assert pyproject["project"]["license"] == license_name
48+
49+
50+
class TestApacheDefault:
51+
"""The default license when the user accepts all defaults is Apache-2.0."""
52+
53+
def test_default_is_apache(self, tmp_path):
54+
# Build data without a `license` key so copier falls back to the
55+
# question's default (Apache-2.0).
56+
data = {k: v for k, v in DEFAULT_DATA.items() if k != "license"}
57+
run_copy(
58+
str(TEMPLATE_ROOT),
59+
tmp_path,
60+
data=data,
61+
defaults=True,
62+
unsafe=True,
63+
vcs_ref="HEAD",
64+
)
65+
license_text = (tmp_path / "LICENSE").read_text(encoding="utf-8")
66+
assert LICENSE_MARKERS["Apache-2.0"] in license_text
67+
pyproject = tomllib.loads(
68+
(tmp_path / "pyproject.toml").read_text(encoding="utf-8")
69+
)
70+
assert pyproject["project"]["license"] == "Apache-2.0"
71+
72+
73+
class TestExistingLicenseFile:
74+
"""When existing_license_file is set, no LICENSE is generated, the user's
75+
existing file is preserved, pyproject records `LicenseRef-Custom` and
76+
points `license-files` at the user's file."""
77+
78+
def _data_with_existing(self, filename: str) -> dict:
79+
# Omit `license` so copier computes the dynamic default
80+
# ("LicenseRef-Custom" when existing_license_file is set).
81+
data = {k: v for k, v in DEFAULT_DATA.items() if k != "license"}
82+
data["existing_license_file"] = filename
83+
return data
84+
85+
def test_no_license_generated_when_existing_file_named(self, tmp_path):
86+
data = self._data_with_existing("LICENSE.md")
87+
run_copy(
88+
str(TEMPLATE_ROOT),
89+
tmp_path,
90+
data=data,
91+
defaults=True,
92+
unsafe=True,
93+
vcs_ref="HEAD",
94+
)
95+
assert not (tmp_path / "LICENSE").exists()
96+
pyproject = tomllib.loads(
97+
(tmp_path / "pyproject.toml").read_text(encoding="utf-8")
98+
)
99+
assert pyproject["project"]["license"] == "LicenseRef-Custom"
100+
assert pyproject["project"]["license-files"] == ["LICENSE.md"]
101+
102+
def test_preexisting_license_md_is_preserved(self, tmp_path):
103+
sentinel = "MY EXISTING LICENSE — DO NOT TOUCH\n"
104+
(tmp_path / "LICENSE.md").write_text(sentinel, encoding="utf-8")
105+
data = self._data_with_existing("LICENSE.md")
106+
run_copy(
107+
str(TEMPLATE_ROOT),
108+
tmp_path,
109+
data=data,
110+
defaults=True,
111+
unsafe=True,
112+
vcs_ref="HEAD",
113+
)
114+
assert not (tmp_path / "LICENSE").exists()
115+
assert (tmp_path / "LICENSE.md").read_text(encoding="utf-8") == sentinel

0 commit comments

Comments
 (0)