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
1 change: 1 addition & 0 deletions changes/3011.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase will now honor the PEP 621 project name as the Briefcase project name.
1 change: 1 addition & 0 deletions changes/3011.removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `project_name` for a project must now be a valid PEP 621 project name. If your existing project name is not a valid PEP 621 project name, a warning will be raised; this will become an error in future.
5 changes: 3 additions & 2 deletions docs/en/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ If you have an application called "My App", with source code in the `src/myapp`

```toml
[tool.briefcase]
project_name = "My Project"
project_name = "my-project"
bundle = "com.example"
version = "0.1"

Expand Down Expand Up @@ -79,7 +79,7 @@ Many platforms will also require a [`license_files`][] definition.

#### `project_name`

The project is the collection of all applications that are described by the briefcase configuration. For projects with a single app, this may be the same as the formal name of the solitary packaged app.
The project is the collection of all applications that are described by the briefcase configuration. For projects with a single app, this may be the same as the app name of the solitary packaged app. It must be a [valid PEP 621 project name](https://packaging.python.org/en/latest/specifications/name-normalization/#name-format); it will be automatically normalized by Briefcase.

#### `version`

Expand Down Expand Up @@ -514,6 +514,7 @@ Many of the keys that exist in Briefcase's configuration have analogous settings

The following `[project]` metadata keys will be used by Briefcase if they are available:

- `name` is normalized and maps to the Briefcase [`project_name`][] setting.
- `version` maps to the same key in Briefcase.
- `license` and `license-files` map to the same key in Briefcase. Legacy formats for these keys will be [coerced into PEP 639 format][license-definitions].
- `authors` The `email` and `name` keys of the first value in the `authors` setting map to [`author`][] and [`author_email`][].
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/commands/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ def build_app_context(self, project_overrides):
module_name, override_value=project_overrides.pop("test_source_dir", None)
)
project_name = self.input_project_name(
formal_name, override_value=project_overrides.pop("project_name", None)
app_name, override_value=project_overrides.pop("project_name", None)
)
description = self.input_description(
override_value=project_overrides.pop("description", None)
Expand Down
9 changes: 5 additions & 4 deletions src/briefcase/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,17 +270,18 @@ def make_project_url(self, bundle, app_name):
"""
return f"https://{self.make_domain(bundle)}/{app_name}"

def input_project_name(self, formal_name, override_value):
def input_project_name(self, app_name, override_value):
return self.console.text_question(
intro=(
"Briefcase can manage projects that contain multiple applications, so "
"we need a Project name.\n"
"\n"
"If you're only planning to have one application in this project, you "
"can use the formal name as the project name."
"can use the app name as the project name."
),
description="Project Name",
default=formal_name,
default=app_name,
validator=self.validate_app_name,
override_value=override_value,
)

Expand Down Expand Up @@ -359,7 +360,7 @@ def build_app_context(self, project_overrides: dict[str, str]) -> dict[str, str]
)

project_name = self.input_project_name(
formal_name, project_overrides.pop("project_name", None)
app_name, project_overrides.pop("project_name", None)
)

description = self.console.text_question(
Expand Down
36 changes: 36 additions & 0 deletions src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from build import BuildBackendException
from build.util import project_wheel_metadata
from packaging.licenses import InvalidLicenseExpression, canonicalize_license_expression
from packaging.utils import InvalidName, canonicalize_name
from packaging.version import InvalidVersion, Version

from briefcase.debuggers.base import BaseDebugger
Expand Down Expand Up @@ -1289,6 +1290,16 @@ def resolve_dynamic_pep621_config(base_path, dynamic, console):
def merge_pep621_config(global_config, pep621_config):
"""Merge a PEP621 configuration into a Briefcase configuration."""

if "name" in pep621_config:
try:
project_name = canonicalize_name(pep621_config["name"], validate=True)
except (InvalidName, TypeError):
raise BriefcaseConfigError(
f"The PEP 621 project name {pep621_config['name']!r} is invalid."
) from None

global_config.setdefault("project_name", project_name)

if requires_python := pep621_config.get("requires-python"):
global_config["requires_python"] = requires_python

Expand Down Expand Up @@ -1344,6 +1355,29 @@ def maybe_update(field, *project_fields):
pass


def warn_invalid_legacy_project_name(global_config, console):
"""Warn if a legacy Briefcase project name is not valid PEP 621 metadata."""
try:
project_name = global_config["project_name"]
except KeyError:
return

try:
canonicalize_name(project_name, validate=True)
except (InvalidName, TypeError):
console.warning_banner(
"Invalid project name",
f"""
{project_name!r} is not a valid PEP 621 project name and cannot be
normalized. Briefcase currently accepts invalid `project_name` values
in the `[tool.briefcase]` configuration, but this may become an error
in a future version.

Update `project_name` to a valid PEP 621 project name.
""",
)


def parse_config(config_file: Path, platform, output_format, console):
"""Parse the briefcase section of the pyproject.toml configuration file.

Expand Down Expand Up @@ -1397,6 +1431,8 @@ def parse_config(config_file: Path, platform, output_format, console):
except KeyError:
pass

warn_invalid_legacy_project_name(global_config, console)

# For consistent results, sort the platforms and formats
all_platforms = sorted(get_platforms().keys())
all_formats = sorted(get_output_formats(platform).keys())
Expand Down
1 change: 1 addition & 0 deletions tests/commands/convert/test_build_app_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def test_overrides_are_used(convert_command):
}
override_input = overrides.copy()
out = convert_command.build_app_context(override_input)

for k, v in overrides.items():
if k == "app_type":
assert not out["console_app"]
Expand Down
14 changes: 7 additions & 7 deletions tests/commands/new/test_build_app_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_question_sequence(new_command):
"My Application", # formal name
"", # app name - accept the default
"org.beeware", # bundle ID
"My Project", # project name
"my-project", # project name
"Cool stuff", # description
"Grace Hopper", # author
"grace@navy.mil", # author email
Expand All @@ -35,7 +35,7 @@ def test_question_sequence(new_command):
"module_name": "myapplication",
"source_dir": "src/myapplication",
"test_source_dir": "tests",
"project_name": "My Project",
"project_name": "my-project",
"url": "https://navy.mil/myapplication",
}

Expand All @@ -51,7 +51,7 @@ def test_question_sequence_with_overrides(new_command):
"formal_name": "My Override App",
"app_name": "myoverrideapp",
"bundle": "net.example",
"project_name": "My Override Project",
"project_name": "my-override-project",
"description": "My override description",
"author": "override, author",
"author_email": "author@override.tld",
Expand All @@ -72,7 +72,7 @@ def test_question_sequence_with_overrides(new_command):
"module_name": "myoverrideapp",
"source_dir": "src/myoverrideapp",
"test_source_dir": "tests",
"project_name": "My Override Project",
"project_name": "my-override-project",
"url": "https://override.example.com",
}

Expand All @@ -90,7 +90,7 @@ def test_question_sequence_with_bad_license_override(new_command):
"formal_name": "My Override App",
"app_name": "myoverrideapp",
"bundle": "net.example",
"project_name": "My Override Project",
"project_name": "my-override-project",
"description": "My override description",
"author": "override, author",
"author_email": "author@override.tld",
Expand All @@ -111,7 +111,7 @@ def test_question_sequence_with_bad_license_override(new_command):
"module_name": "myoverrideapp",
"source_dir": "src/myoverrideapp",
"test_source_dir": "tests",
"project_name": "My Override Project",
"project_name": "my-override-project",
"url": "https://override.example.com",
}

Expand All @@ -135,7 +135,7 @@ def test_question_sequence_with_no_user_input(new_command):
"module_name": "helloworld",
"source_dir": "src/helloworld",
"test_source_dir": "tests",
"project_name": "Hello World",
"project_name": "helloworld",
"url": "https://example.com/helloworld",
}

Expand Down
51 changes: 51 additions & 0 deletions tests/config/test_merge_pep621_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest

from briefcase.config import merge_pep621_config
from briefcase.exceptions import BriefcaseConfigError


def test_empty():
Expand All @@ -10,6 +13,54 @@ def test_empty():
assert briefcase_config == {"key": "value"}


def test_project_name_is_normalized():
"""The PEP 621 project name is normalized for use by Briefcase."""
briefcase_config = {"key": "value"}

merge_pep621_config(
briefcase_config,
{"name": "Some.Project_Name"},
)

assert briefcase_config == {
"key": "value",
"project_name": "some-project-name",
}


@pytest.mark.parametrize(
("project_name", "representation"),
[
("not a valid name!", "'not a valid name!'"),
(42, "42"),
],
)
def test_invalid_project_name(project_name, representation):
"""An invalid PEP 621 project name raises a clean configuration error."""
briefcase_config = {"key": "value"}

with pytest.raises(
BriefcaseConfigError,
match=rf"The PEP 621 project name {representation} is invalid\.",
):
merge_pep621_config(
briefcase_config,
{"name": project_name},
)


def test_legacy_project_name_takes_priority():
"""A legacy project name takes priority over PEP 621 project metadata."""
briefcase_config = {"project_name": "Legacy_Name"}

merge_pep621_config(
briefcase_config,
{"name": "PEP.621.Name"},
)

assert briefcase_config == {"project_name": "Legacy_Name"}


def test_base_keys():
"If the PEP621 config provides keys, they are added"
briefcase_config = {"key": "value"}
Expand Down
Loading
Loading