Repository: pdm-project/pdm
Pull request #3440 (2025-04-01 09:49:54)

fix: set project name if specified with URL
Signed-off-by: Frost Ming <me@frostming.com>

## Pull Request Checklist

- [ ] A news fragment is added in `news/` describing what is new.
- [ ] Test cases added for changed code.

## Describe what you have changed in this PR.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/3440?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 85.23%. Comparing base [(`8ebd9c6`)](https://app.codecov.io/gh/pdm-project/pdm/commit/8ebd9c60208fbd3c61ffd56d852db1a3f1d962e7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) to head [(`901c5ed`)](https://app.codecov.io/gh/pdm-project/pdm/commit/901c5ed74f0fe089960681ce0465cfd9ff93849d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3440      +/-   ##
==========================================
- Coverage   85.25%   85.23%   -0.02%     
==========================================
  Files         112      112              
  Lines       11548    11549       +1     
  Branches     2533     2534       +1     
==========================================
- Hits         9845     9844       -1     
- Misses       1176     1177       +1     
- Partials      527      528       +1     
```

| [Flag](https://app.codecov.io/gh/pdm-project/pdm/pull/3440/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/pdm-project/pdm/pull/3440/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | `85.03% <100.00%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/3440?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/news/3407.bugfix.md b/news/3407.bugfix.md
new file mode 100644
index 00000000..9ada5b18
--- /dev/null
+++ b/news/3407.bugfix.md
@@ -0,0 +1,1 @@
+Installation error for local plugins specified with file URL without a name.
diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py
index 0eb2ad9a..e52b3a70 100644
--- a/src/pdm/cli/commands/run.py
+++ b/src/pdm/cli/commands/run.py
@@ -163,7 +163,9 @@ class TaskRunner:
             return self.project.environment
         tool_config = metadata.pop("tool", {})
         script_project = self.project.core.create_project()
-        script_project.pyproject.set_data({"project": {"name": "temp-project", **metadata}, "tool": tool_config})
+        script_project.pyproject.set_data(
+            {"project": {"name": "temp-project", **metadata, "version": "0.0.0"}, "tool": tool_config}
+        )
         md5_kwargs = {"usedforsecurity": False}
         venv_name = hashlib.md5(os.path.realpath(script_file).encode("utf-8"), **md5_kwargs).hexdigest()
         venv_backend = BACKENDS[script_project.config["venv.backend"]](script_project, None)
diff --git a/src/pdm/models/requirements.py b/src/pdm/models/requirements.py
index e6c625e9..034fc538 100644
--- a/src/pdm/models/requirements.py
+++ b/src/pdm/models/requirements.py
@@ -310,11 +310,6 @@ class FileRequirement(Requirement):
             if not self.url and path.is_absolute():
                 self.url = path.as_uri() + fragments
                 self.path = path
-            # For relative path, we don't resolve URL now, so the path may still contain fragments,
-            # it will be handled in `relocate()` method.
-            result = Setup.from_directory(self.absolute_path)  # type: ignore[arg-type]
-            if result.name:
-                self.name = result.name
         else:
             url = url_without_fragments(self.url)
             relpath = get_relative_path(url)
@@ -326,6 +321,13 @@ class FileRequirement(Requirement):
             else:
                 self.path = Path(relpath)
 
+        if self.path is not None:
+            # For relative path, we don't resolve URL now, so the path may still contain fragments,
+            # it will be handled in `relocate()` method.
+            result = Setup.from_directory(self.absolute_path)  # type: ignore[arg-type]
+            if result.name:
+                self.name = result.name
+
         if self.url:
             self._parse_name_from_url()
 


Test patch:
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 1692c374..5e87ba8d 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -118,3 +118,31 @@ def test_project_plugin_library(pdm, project, core, monkeypatch):
         core.load_plugins()
         result = pdm(["hello", "Frost"], strict=True)
     assert result.stdout.strip() == "Hello, Frost!"
+
+
+@pytest.mark.parametrize(
+    "req_str",
+    [
+        "-e file:///${PROJECT_ROOT}/plugins/test-plugin-pdm",
+        "-e test_plugin_pdm @ file:///${PROJECT_ROOT}/plugins/test-plugin-pdm",
+    ],
+)
+@pytest.mark.usefixtures("local_finder")
+def test_install_local_plugin_without_name(pdm, project, core, req_str):
+    import shutil
+
+    from . import FIXTURES
+
+    test_plugin_path = FIXTURES / "projects" / "test-plugin-pdm"
+    project.root.joinpath("plugins").mkdir(exist_ok=True)
+    shutil.copytree(test_plugin_path, project.root / "plugins" / "test-plugin-pdm", dirs_exist_ok=True)
+
+    project.pyproject.settings["plugins"] = [req_str]
+    project.pyproject.write()
+    with cd(project.root):
+        result = pdm(["install", "--plugins", "-vv"], obj=project, strict=True)
+
+        assert project.root.joinpath(".pdm-plugins").exists()
+        core.load_plugins()
+        result = pdm(["hello", "--name", "Frost"], strict=True)
+    assert result.stdout.strip() == "Hello, Frost"