diff --git a/news/2394.feature.md b/news/2394.feature.md new file mode 100644 index 0000000000..6504231eb1 --- /dev/null +++ b/news/2394.feature.md @@ -0,0 +1 @@ +We now use the `package-type` field in the `tool.pdm` table to differentiate between library and application projects. diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py index 5976922383..9c11f6dc09 100644 --- a/src/pdm/cli/actions.py +++ b/src/pdm/cli/actions.py @@ -204,7 +204,7 @@ def do_sync( clean=clean, dry_run=dry_run, no_editable=no_editable, - install_self=not no_self and bool(project.name), + install_self=not no_self and project.is_library, reinstall=reinstall, only_keep=only_keep, fail_fast=fail_fast, diff --git a/src/pdm/cli/commands/add.py b/src/pdm/cli/commands/add.py index d5c8f17c46..751867f0e8 100644 --- a/src/pdm/cli/commands/add.py +++ b/src/pdm/cli/commands/add.py @@ -128,8 +128,8 @@ def do_add( if editables: raise PdmUsageError("Cannot add editables to the default or optional dependency group") for r in [parse_requirement(line, True) for line in editables] + [parse_requirement(line) for line in packages]: - if project.name and normalize_name(project.name) == r.key and not r.extras: - project.core.ui.warn(f"Package [req]{project.name}[/] is the project itself.") + if project.is_library and normalize_name(name := project.name) == r.key and not r.extras: + project.core.ui.warn(f"Package [req]{name}[/] is the project itself.") continue if r.is_file_or_url: r.relocate(project.backend) # type: ignore[attr-defined] diff --git a/src/pdm/cli/commands/init.py b/src/pdm/cli/commands/init.py index 9fb33501fa..79583aeda1 100644 --- a/src/pdm/cli/commands/init.py +++ b/src/pdm/cli/commands/init.py @@ -110,6 +110,8 @@ def get_metadata_from_input(self, project: Project, options: argparse.Namespace) from pdm.formats.base import array_of_inline_tables, make_array, make_inline_table is_library = options.lib + name = self.ask("Project name", project.root.name) + version = self.ask("Project version", "0.1.0") if not is_library and self.interactive: is_library = termui.confirm( "Is the project a library that is installable?\n" @@ -119,8 +121,6 @@ def get_metadata_from_input(self, project: Project, options: argparse.Namespace) build_backend: type[BuildBackend] | None = None python = project.python if is_library: - name = self.ask_project(project) - version = self.ask("Project version", "0.1.0") description = self.ask("Project description", "") if options.backend: build_backend = get_backend(options.backend) @@ -141,7 +141,7 @@ def get_metadata_from_input(self, project: Project, options: argparse.Namespace) build_backend = DEFAULT_BACKEND default_python_requires = f">={python.major}.{python.minor}" else: - name, version, description = "", "", "" + description = "" default_python_requires = f"=={python.major}.{python.minor}.*" license = self.ask("License(SPDX name)", "MIT") @@ -154,16 +154,18 @@ def get_metadata_from_input(self, project: Project, options: argparse.Namespace) "project": { "name": name, "version": version, - "description": description, "authors": array_of_inline_tables([{"name": author, "email": email}]), "license": make_inline_table({"text": license}), "dependencies": make_array([], True), }, + "tool": {"pdm": {"package-type": "library" if is_library else "application"}}, } if python_requires and python_requires != "*": get_specifier(python_requires) - data["project"]["requires-python"] = python_requires + data["project"]["requires-python"] = python_requires # type: ignore[index] + if description: + data["project"]["description"] = description # type: ignore[index] if build_backend is not None: data["build-system"] = cast(dict, build_backend.build_system()) diff --git a/src/pdm/cli/commands/show.py b/src/pdm/cli/commands/show.py index 7f2b8ba3b6..64ac29e540 100644 --- a/src/pdm/cli/commands/show.py +++ b/src/pdm/cli/commands/show.py @@ -51,8 +51,8 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: latest_stable = next(filter(filter_stable, best_match.applicable), None) metadata = latest.prepare(project.environment).metadata else: - if not project.name: - raise PdmUsageError("This project is not a package") + if not project.is_library: + raise PdmUsageError("This project is not a library") package = normalize_name(project.name) metadata = project.make_self_candidate(False).prepare(project.environment).prepare_metadata(True) latest_stable = None diff --git a/src/pdm/cli/utils.py b/src/pdm/cli/utils.py index cafa036c5e..7a5a310573 100644 --- a/src/pdm/cli/utils.py +++ b/src/pdm/cli/utils.py @@ -326,7 +326,7 @@ def add_package_to_reverse_tree( def package_is_project(package: Package, project: Project) -> bool: - return project.name is not None and package.name == normalize_name(project.name) + return project.is_library and package.name == normalize_name(project.name) def _format_forward_dependency_graph( diff --git a/src/pdm/models/repositories.py b/src/pdm/models/repositories.py index a204ef57b4..63681ddaa9 100644 --- a/src/pdm/models/repositories.py +++ b/src/pdm/models/repositories.py @@ -138,7 +138,7 @@ def _find_candidates(self, requirement: Requirement, minimal_version: bool) -> I def is_this_package(self, requirement: Requirement) -> bool: """Whether the requirement is the same as this package""" project = self.environment.project - return requirement.is_named and project.name is not None and requirement.key == normalize_name(project.name) + return requirement.is_named and project.is_library and requirement.key == normalize_name(project.name) def make_this_candidate(self, requirement: Requirement) -> Candidate: """Make a candidate for this package. @@ -147,7 +147,6 @@ def make_this_candidate(self, requirement: Requirement) -> Candidate: from unearth import Link project = self.environment.project - assert project.name link = Link.from_path(project.root) candidate = make_candidate(requirement, project.name, link=link) candidate.prepare(self.environment).metadata @@ -287,7 +286,7 @@ def _get_dependency_from_local_package(self, candidate: Candidate) -> CandidateI """Adds the local package as a candidate only if the candidate name is the same as the local package.""" project = self.environment.project - if not project.name or candidate.name != project.name: + if not project.is_library or candidate.name != project.name: raise CandidateInfoNotFound(candidate) from None reqs = project.pyproject.metadata.get("dependencies", []) diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py index dc8db59db3..94bf698639 100644 --- a/src/pdm/project/core.py +++ b/src/pdm/project/core.py @@ -140,7 +140,7 @@ def project_config(self) -> Config: return config @property - def name(self) -> str | None: + def name(self) -> str: return self.pyproject.metadata.get("name") @property @@ -679,3 +679,7 @@ def meta(self) -> dict[str, Any]: def tool_settings(self) -> dict[str, Any]: deprecation_warning("project.tool_settings is deprecated, use project.pyproject.settings instead", stacklevel=2) return self.pyproject.settings + + @property + def is_library(self) -> bool: + return bool(self.name) and self.pyproject.settings.get("package-type", "library") == "library" diff --git a/src/pdm/resolver/core.py b/src/pdm/resolver/core.py index 700ff12887..bde487cdc8 100644 --- a/src/pdm/resolver/core.py +++ b/src/pdm/resolver/core.py @@ -45,7 +45,9 @@ def resolve( mapping = cast(Dict[str, Candidate], result.mapping) mapping.pop("python", None) - local_name = normalize_name(repository.environment.project.name) if repository.environment.project.name else None + local_name = ( + normalize_name(repository.environment.project.name) if repository.environment.project.is_library else None + ) for key, candidate in list(result.mapping.items()): if key is None: continue diff --git a/tests/cli/test_init.py b/tests/cli/test_init.py index 521b7fe0e5..cb86527d3c 100644 --- a/tests/cli/test_init.py +++ b/tests/cli/test_init.py @@ -8,7 +8,7 @@ def test_init_validate_python_requires(project_no_init, pdm): - result = pdm(["init"], input="\n\n\n\n\n3.7\n", obj=project_no_init) + result = pdm(["init"], input="\n\n\n\n\n\n\n3.7\n", obj=project_no_init) assert result.exit_code != 0 assert "InvalidSpecifier" in result.stderr @@ -18,19 +18,20 @@ def test_init_command(project_no_init, pdm, mocker): "pdm.cli.commands.init.get_user_email_from_git", return_value=("Testing", "me@example.org"), ) - pdm(["init"], input="\n\n\n\n\n\n", strict=True, obj=project_no_init) + pdm(["init"], input="\ntest-project\n\n\n\n\n\n\n", strict=True, obj=project_no_init) python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}" data = { "project": { "authors": [{"email": "me@example.org", "name": "Testing"}], "dependencies": [], - "description": "", + "description": "Default template for PDM package", "license": {"text": "MIT"}, - "name": "", + "name": "test-project", "requires-python": f"=={python_version}.*", "readme": "README.md", - "version": "", + "version": "0.1.0", }, + "tool": {"pdm": {"package-type": "application"}}, } with open(project_no_init.root.joinpath("pyproject.toml"), "rb") as fp: @@ -44,7 +45,7 @@ def test_init_command_library(project_no_init, pdm, mocker): ) result = pdm( ["init"], - input="\ny\ntest-project\n\nTest Project\n1\n\n\n\n\n", + input="\ntest-project\n\ny\nTest Project\n1\n\n\n\n\n", obj=project_no_init, ) assert result.exit_code == 0 @@ -61,6 +62,7 @@ def test_init_command_library(project_no_init, pdm, mocker): "version": "0.1.0", }, "build-system": {"build-backend": "setuptools.build_meta", "requires": ["setuptools>=61", "wheel"]}, + "tool": {"pdm": {"package-type": "library"}}, } with open(project_no_init.root.joinpath("pyproject.toml"), "rb") as fp: @@ -89,13 +91,14 @@ def test_init_non_interactive(project_no_init, pdm, mocker): "project": { "authors": [{"email": "me@example.org", "name": "Testing"}], "dependencies": [], - "description": "", + "description": "Default template for PDM package", "license": {"text": "MIT"}, - "name": "", + "name": project_no_init.root.name, "requires-python": f"=={python_version}.*", "readme": "README.md", - "version": "", + "version": "0.1.0", }, + "tool": {"pdm": {"package-type": "application"}}, } with open(project_no_init.root.joinpath("pyproject.toml"), "rb") as fp: @@ -105,7 +108,7 @@ def test_init_non_interactive(project_no_init, pdm, mocker): def test_init_auto_create_venv(project_no_init, pdm, mocker): mocker.patch("pdm.models.python.PythonInfo.get_venv", return_value=None) project_no_init.project_config["python.use_venv"] = True - result = pdm(["init"], input="\n\n\n\n\n\n\n", obj=project_no_init) + result = pdm(["init"], input="\n\n\n\n\n\n\n\n\n", obj=project_no_init) assert result.exit_code == 0 assert project_no_init.python.executable.parent.parent == project_no_init.root / ".venv" assert ".pdm-python" in (project_no_init.root / ".gitignore").read_text() @@ -116,7 +119,7 @@ def test_init_auto_create_venv_specify_python(project_no_init, pdm, mocker): project_no_init.project_config["python.use_venv"] = True result = pdm( ["init", f"--python={PYTHON_VERSION}"], - input="\n\n\n\n\n\n", + input="\n\n\n\n\n\n\n\n", obj=project_no_init, ) assert result.exit_code == 0 @@ -127,7 +130,7 @@ def test_init_auto_create_venv_answer_no(project_no_init, pdm, mocker): mocker.patch("pdm.models.python.PythonInfo.get_venv", return_value=None) creator = mocker.patch("pdm.cli.commands.venv.backends.Backend.create") project_no_init.project_config["python.use_venv"] = True - result = pdm(["init"], input="\nn\n\n\n\n\n\n\n", obj=project_no_init) + result = pdm(["init"], input="\nn\n\n\n\n\n\n\n\n\n", obj=project_no_init) assert result.exit_code == 0 creator.assert_not_called() assert project_no_init.python.executable.parent.parent != project_no_init.root / ".venv" diff --git a/tests/test_signals.py b/tests/test_signals.py index 25ee7ca9fc..2d63c47e58 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -8,7 +8,7 @@ def test_post_init_signal(project_no_init, pdm): mock_handler = mock.Mock() with signals.post_init.connected_to(mock_handler): - result = pdm(["init"], input="\n\n\n\n\n\n", obj=project_no_init) + result = pdm(["init"], input="\n\n\n\n\n\n\n\n", obj=project_no_init) assert result.exit_code == 0 mock_handler.assert_called_once_with(project_no_init, hooks=mock.ANY)