Skip to content

Commit

Permalink
Merge pull request #1812 from MarieRoald/pep621-license-file
Browse files Browse the repository at this point in the history
Support specifying the license file in the pyproject.toml file
  • Loading branch information
freakboy3742 authored May 27, 2024
2 parents af84adb + 24b5e2f commit 31beb73
Show file tree
Hide file tree
Showing 33 changed files with 435 additions and 62 deletions.
1 change: 1 addition & 0 deletions changes/1812.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The name of the license file can now be specified using a PEP 621-compliant format for the ``license`` setting.
1 change: 1 addition & 0 deletions changes/1812.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The format for the ``license`` field has been converted to PEP 621 format. Existing projects that specify ``license`` as a string should update their configurations to point at the generated license file using ``license.file = "LICENSE"``.
1 change: 1 addition & 0 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ def parse_config(self, filename, overrides):
config_file,
platform=self.platform,
output_format=self.output_format,
logger=self.logger,
)

# Create the global config
Expand Down
10 changes: 1 addition & 9 deletions src/briefcase/commands/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,16 +647,8 @@ def migrate_necessary_files(self, project_dir, test_source_dir, module_name):

# Copy license file if not already there
license_file = self.pep621_data.get("license", {}).get("file")
if license_file is not None and Path(license_file).name != "LICENSE":
self.logger.warning(
f"\nLicense file found in '{self.base_path}', but its name is "
f"'{Path(license_file).name}', not 'LICENSE'. Briefcase will create a "
"template 'LICENSE' file, but you might want to consider renaming the "
"existing file."
)
copy2(project_dir / "LICENSE", self.base_path / "LICENSE")

elif not (self.base_path / "LICENSE").exists():
if license_file is None and not (self.base_path / "LICENSE").exists():
self.logger.warning(
f"\nLicense file not found in '{self.base_path}'. "
"Briefcase will create a template 'LICENSE' file."
Expand Down
21 changes: 19 additions & 2 deletions src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def __init__(
project_name,
version,
bundle,
license=None,
url=None,
author=None,
author_email=None,
Expand All @@ -161,6 +162,7 @@ def __init__(
self.url = url
self.author = author
self.author_email = author_email
self.license = license

# Version number is PEP440 compliant:
if not is_pep440_canonical_version(self.version):
Expand All @@ -182,6 +184,7 @@ def __init__(
bundle,
description,
sources,
license,
formal_name=None,
url=None,
author=None,
Expand Down Expand Up @@ -223,6 +226,7 @@ def __init__(
self.test_requires = test_requires
self.supported = supported
self.long_description = long_description
self.license = license

if not is_valid_app_name(self.app_name):
raise BriefcaseConfigError(
Expand Down Expand Up @@ -388,7 +392,7 @@ def maybe_update(field, *project_fields):

# Keys that map directly
maybe_update("description", "description")
maybe_update("license", "license", "text")
maybe_update("license", "license")
maybe_update("url", "urls", "Homepage")
maybe_update("version", "version")

Expand Down Expand Up @@ -421,7 +425,7 @@ def maybe_update(field, *project_fields):
pass


def parse_config(config_file, platform, output_format):
def parse_config(config_file, platform, output_format, logger):
"""Parse the briefcase section of the pyproject.toml configuration file.
This method only does basic structural parsing of the TOML, looking for,
Expand Down Expand Up @@ -546,4 +550,17 @@ def parse_config(config_file, platform, output_format):
# of configurations that are being handled.
app_configs[app_name] = config

old_license_format = False
for config in [global_config, *app_configs.values()]:
if isinstance(config.get("license"), str):
config["license"] = {"file": "LICENSE"}
old_license_format = True

if old_license_format:
logger.warning(
"Your app configuration has a `license` field that is specified as a string. "
"Briefcase now uses PEP 621 format for license definitions. To silence this "
'warning, replace the `license` declaration with `license.file = "LICENSE".'
)

return global_config, app_configs
42 changes: 34 additions & 8 deletions src/briefcase/platforms/linux/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,16 +704,38 @@ def build_app(self, app: AppConfig, **kwargs):
doc_folder.mkdir(parents=True, exist_ok=True)

with self.input.wait_bar("Installing license..."):
license_file = self.base_path / "LICENSE"
if license_file.is_file():
self.tools.shutil.copy(license_file, doc_folder / "copyright")
if license_file := app.license.get("file"):
license_file = self.base_path / license_file
if license_file.is_file():
self.tools.shutil.copy(license_file, doc_folder / "copyright")
else:
raise BriefcaseCommandError(
f"""\
Your `pyproject.toml` specifies a license file of {str(license_file.relative_to(self.base_path))!r}.
However, this file does not exist.
Ensure you have correctly spelled the filename in your `license.file` setting.
"""
)
elif license_text := app.license.get("text"):
(doc_folder / "copyright").write_text(license_text, encoding="utf-8")
if len(license_text.splitlines()) <= 1:
self.logger.warning(
"""
Your app specifies a license using `license.text`, but the value doesn't appear to be a
full license. Briefcase will generate a `copyright` file for your project; you should
ensure that the contents of this file is adequate.
"""
)
else:
raise BriefcaseCommandError(
"""\
Your project does not contain a LICENSE file.
Your project does not contain a LICENSE definition.
Create a file named `LICENSE` in the same directory as your `pyproject.toml`
with your app's licensing terms.
with your app's licensing terms, and set `license.file = 'LICENSE'` in your
app's configuration.
"""
)

Expand Down Expand Up @@ -792,7 +814,11 @@ class LinuxSystemRunCommand(LinuxSystemMixin, RunCommand):
supported_host_os_reason = "Linux system projects can only be executed on Linux."

def run_app(
self, app: AppConfig, test_mode: bool, passthrough: list[str], **kwargs
self,
app: AppConfig,
test_mode: bool,
passthrough: list[str],
**kwargs,
):
"""Start the application.
Expand Down Expand Up @@ -1037,7 +1063,7 @@ def _package_rpm(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-wind
f"Release: {getattr(app, 'revision', 1)}%{{?dist}}",
f"Summary: {app.description}",
"",
f"License: {getattr(app, 'license', 'Unknown')}",
"License: Unknown", # TODO: Add license information (see #1829)
f"URL: {app.url}",
"Source0: %{name}-%{version}.tar.gz",
"",
Expand Down Expand Up @@ -1196,7 +1222,7 @@ def _package_pkg(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-wind
f'pkgdesc="{app.description}"',
f"arch=('{self.pkg_abi(app)}')",
f'url="{app.url}"',
f"license=('{app.license}')",
"license=('Unknown')",
f"depends=({system_runtime_requires})",
"changelog=CHANGELOG",
'source=("$pkgname-$pkgver.tar.gz")',
Expand Down
1 change: 1 addition & 0 deletions tests/commands/base/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ def my_app():
version="1.2.3",
description="This is a simple app",
sources=["src/my_app"],
license={"file": "LICENSE"},
)
2 changes: 2 additions & 0 deletions tests/commands/base/test_finalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def first_app():
version="0.0.1",
description="The first simple app",
sources=["src/first"],
license={"file": "LICENSE"},
)


Expand All @@ -24,6 +25,7 @@ def second_app():
version="0.0.2",
description="The second simple app",
sources=["src/second"],
license={"file": "LICENSE"},
)


Expand Down
5 changes: 5 additions & 0 deletions tests/commands/base/test_parse_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_incomplete_global_config(base_command):
[tool.briefcase]
version = "1.2.3"
description = "A sample app"
license.file = "LICENSE"
[tool.briefcase.app.my-app]
""",
Expand All @@ -47,6 +48,7 @@ def test_incomplete_config(base_command):
version = "1.2.3"
bundle = "com.example"
description = "A sample app"
license.file = "LICENSE"
[tool.briefcase.app.my-app]
""",
Expand All @@ -71,6 +73,7 @@ def test_parse_config(base_command):
description = "A sample app"
bundle = "org.beeware"
mystery = 'default'
license.file = "LICENSE"
[tool.briefcase.app.firstapp]
sources = ['src/firstapp']
Expand Down Expand Up @@ -127,6 +130,7 @@ def test_parse_config_with_overrides(base_command):
description = "A sample app"
bundle = "org.beeware"
mystery = 'default'
license.file = "LICENSE"
[tool.briefcase.app.firstapp]
sources = ['src/firstapp']
Expand Down Expand Up @@ -197,6 +201,7 @@ def test_parse_config_with_invalid_override(base_command):
description = "A sample app"
bundle = "org.beeware"
mystery = 'default'
license.file = "LICENSE"
[tool.briefcase.app.firstapp]
sources = ['src/firstapp']
Expand Down
1 change: 1 addition & 0 deletions tests/commands/build/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def second_app_config():
version="0.0.2",
description="The second simple app",
sources=["src/second"],
license={"file": "LICENSE"},
)


Expand Down
57 changes: 52 additions & 5 deletions tests/commands/convert/test_migrate_necessary_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,31 @@ def test_warning_without_license_file(


@pytest.mark.parametrize("test_source_dir", ["tests"])
def test_pep621_wrong_license_filename(
def test_file_is_copied_if_no_license_file_specified(
convert_command,
project_dir_with_files,
dummy_app_name,
test_source_dir,
):
"""A license file is copied if no license file is specified in pyproject.toml."""
create_file(convert_command.base_path / "CHANGELOG", "")
assert not (convert_command.base_path / "LICENSE").exists()
convert_command.migrate_necessary_files(
project_dir_with_files,
test_source_dir,
dummy_app_name,
)
assert (convert_command.base_path / "LICENSE").exists()


@pytest.mark.parametrize("test_source_dir", ["tests"])
def test_pep621_specified_license_filename(
convert_command,
project_dir_with_files,
dummy_app_name,
test_source_dir,
):
"""No license file is copied if a license file is specified in pyproject.toml."""
convert_command.logger.warning = mock.MagicMock()
license_name = "LICENSE.txt"
create_file(convert_command.base_path / license_name, "")
Expand All @@ -121,10 +140,34 @@ def test_pep621_wrong_license_filename(
test_source_dir,
dummy_app_name,
)
assert not (convert_command.base_path / "LICENSE").exists()


@pytest.mark.parametrize("test_source_dir", ["tests"])
def test_pep621_specified_license_text(
convert_command,
project_dir_with_files,
dummy_app_name,
test_source_dir,
):
"""A license file is copied if the license is specified as text and no LICENSE file
exists."""
convert_command.logger.warning = mock.MagicMock()
create_file(
convert_command.base_path / "pyproject.toml",
'[project]\nlicense = { text = "New BSD" }',
)
create_file(convert_command.base_path / "CHANGELOG", "")
convert_command.migrate_necessary_files(
project_dir_with_files,
test_source_dir,
dummy_app_name,
)
assert (convert_command.base_path / "LICENSE").exists()

convert_command.logger.warning.assert_called_once_with(
f"\nLicense file found in '{convert_command.base_path}', but its name is "
f"'{license_name}', not 'LICENSE'. Briefcase will create a template 'LICENSE' "
"file, but you might want to consider renaming the existing file."
f"\nLicense file not found in '{convert_command.base_path}'. "
"Briefcase will create a template 'LICENSE' file."
)


Expand Down Expand Up @@ -162,7 +205,11 @@ def test_no_warning_with_license_and_changelog_file(
"""No warning is raised if both license file and changelog file is present."""
convert_command.logger.warning = mock.MagicMock()

create_file(convert_command.base_path / "LICENSE", "")
create_file(
convert_command.base_path / "pyproject.toml",
'[project]\nlicense = { file = "LICENSE.txt" }',
)
create_file(convert_command.base_path / "LICENSE.txt", "")
create_file(convert_command.base_path / "CHANGELOG", "")
convert_command.migrate_necessary_files(
project_dir_with_files,
Expand Down
3 changes: 3 additions & 0 deletions tests/commands/create/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,15 @@ def tracking_create_command(tmp_path, mock_git, monkeypatch_tool_host_os):
version="0.0.1",
description="The first simple app",
sources=["src/first"],
license={"file": "LICENSE"},
),
"second": AppConfig(
app_name="second",
bundle="com.example",
version="0.0.2",
description="The second simple app",
sources=["src/second"],
license={"file": "LICENSE"},
),
},
)
Expand All @@ -223,6 +225,7 @@ def myapp():
url="https://example.com",
author="First Last",
author_email="first@example.com",
license={"file": "LICENSE"},
)


Expand Down
1 change: 1 addition & 0 deletions tests/commands/create/test_create_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def test_create_app_not_supported(tracking_create_command, tmp_path):
description="The third simple app",
sources=["src/third"],
supported=False,
license={"file": "LICENSE"},
)
)

Expand Down
1 change: 1 addition & 0 deletions tests/commands/create/test_generate_app_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def full_context():
"custom_permissions": {},
"requests": {},
"document_types": {},
"license": {"file": "LICENSE"},
# Properties of the generating environment
"python_version": platform.python_version(),
"host_arch": "gothic",
Expand Down
Loading

0 comments on commit 31beb73

Please sign in to comment.