Repository: pdm-project/pdm
Pull request #2012 (2023-06-13 09:20:05)

Refactor deprecated `warn` method and use appropriate warnings class
## 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.


sanmai-NL: @frostming May we simplify more of the logging and warnings use with https://docs.python.org/3/library/logging.html#logging.captureWarnings?

E.g., at [termui.logger.warning(message)](https://github.com/sanmai-NL/pdm/blob/e738c3769eb048257e881ba2b5337991b344ecb9/src/pdm/models/candidates.py#L550-L551)?
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/2012?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
Patch coverage: **`50.00`**% and no project coverage change.
> Comparison is base [(`f1216c2`)](https://app.codecov.io/gh/pdm-project/pdm/commit/f1216c23a228ed618bdee8dbb273c8486c09e875?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) 85.18% compared to head [(`e738c37`)](https://app.codecov.io/gh/pdm-project/pdm/pull/2012?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) 85.18%.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?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    #2012   +/-   ##
=======================================
  Coverage   85.18%   85.18%           
=======================================
  Files          99       99           
  Lines        9377     9377           
  Branches     2047     2047           
=======================================
  Hits         7988     7988           
  Misses        942      942           
  Partials      447      447           
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `84.98% <50.00%> (ø)` | |

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.

| [Impacted Files](https://app.codecov.io/gh/pdm-project/pdm/pull/2012?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [src/pdm/models/candidates.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2012?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9tb2RlbHMvY2FuZGlkYXRlcy5weQ==) | `86.91% <25.00%> (ø)` | |
| [src/pdm/utils.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2012?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS91dGlscy5weQ==) | `84.27% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/2012?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](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).

frostming: > * A news fragment is added in `news/` describing what is new.

Can you please add a news fragment?

Patch:
diff --git a/README.md b/README.md
index e3ff0215..e2c07361 100644
--- a/README.md
+++ b/README.md
@@ -85,17 +85,17 @@ Like Pip, PDM provides an installation script that will install PDM into an isol
 **For Linux/Mac**
 
 ```bash
-curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -
+curl -sSL https://pdm.fming.dev/dev/install-pdm.py | python3 -
 ```
 
 **For Windows**
 
 ```powershell
-(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python -
+(Invoke-WebRequest -Uri https://pdm.fming.dev/dev/install-pdm.py -UseBasicParsing).Content | python -
 ```
 
 For security reasons, you should verify the checksum of `install-pdm.py`.
-The sha256 checksum is: `965900c551b4d1ba5127ecf95c127c99b5a1cccef4c183a5ecf447e876fa3160`
+It can be downloaded from [install-pdm.py.sha256](https://pdm.fming.dev/dev/install-pdm.py.sha256).
 
 The installer will install PDM into the user site and the location depends on the system:
 
diff --git a/README_zh.md b/README_zh.md
index 292971fc..4e6aec4c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -73,17 +73,17 @@ PDM 需要 Python 3.7 或更高版本。
 **Linux/Mac 安装命令**
 
 ```bash
-curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -
+curl -sSL https://pdm.fming.dev/dev/install-pdm.py | python3 -
 ```
 
 **Windows 安装命令**
 
 ```powershell
-(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python -
+(Invoke-WebRequest -Uri https://pdm.fming.dev/dev/install-pdm.py -UseBasicParsing).Content | python -
 ```
 
 为安全起见，你应该检查 `install-pdm.py` 文件的正确性。
-SHA256 校验和: `965900c551b4d1ba5127ecf95c127c99b5a1cccef4c183a5ecf447e876fa3160`
+校验和文件下载地址：[install-pdm.py.sha256](https://pdm.fming.dev/dev/install-pdm.py.sha256)
 
 默认情况下，此脚本会将 PDM 安装在 Python 的用户目录下，具体位置取决于当前系统：
 
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 500a773e..1f771eb3 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -38,17 +38,26 @@ Like Pip, PDM provides an installation script that will install PDM into an isol
 === "Linux/Mac"
 
     ```bash
-    curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -
+    curl -sSL https://pdm.fming.dev/dev/install-pdm.py | python3 -
     ```
 
 === "Windows"
 
     ```powershell
-    (Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python -
+    (Invoke-WebRequest -Uri https://pdm.fming.dev/dev/install-pdm.py -UseBasicParsing).Content | python -
     ```
 
 For security reasons, you should verify the checksum of `install-pdm.py`.
-The sha256 checksum is: `965900c551b4d1ba5127ecf95c127c99b5a1cccef4c183a5ecf447e876fa3160`
+It can be downloaded from [install-pdm.py.sha256](https://pdm.fming.dev/dev/install-pdm.py.sha256).
+
+For example, on Linux/Mac:
+
+```bash
+curl -sSLO https://pdm.fming.dev/dev/install-pdm.py
+curl -sSL https://pdm.fming.dev/dev/install-pdm.py.sha256 | shasum -a 256 -c -
+# Run the installer
+python3 install-pdm.py [options]
+```
 
 The installer will install PDM into the user site and the location depends on the system:
 
diff --git a/docs/docs/install-pdm.py b/docs/docs/install-pdm.py
new file mode 100644
index 00000000..c4fc2351
--- /dev/null
+++ b/docs/docs/install-pdm.py
@@ -0,0 +1,456 @@
+from __future__ import annotations
+
+import argparse
+import dataclasses
+import io
+import json
+import os
+import platform
+import re
+import shutil
+import site
+import subprocess
+import sys
+import urllib.request
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Sequence
+
+if sys.version_info < (3, 7):
+    sys.exit("Python 3.7 or above is required to install PDM.")
+
+_plat = platform.system()
+MACOS = _plat == "Darwin"
+WINDOWS = _plat == "Windows"
+REPO = "https://github.com/pdm-project/pdm"
+JSON_URL = "https://pypi.org/pypi/pdm/json"
+
+FOREGROUND_COLORS = {
+    "black": 30,
+    "red": 31,
+    "green": 32,
+    "yellow": 33,
+    "blue": 34,
+    "magenta": 35,
+    "cyan": 36,
+    "white": 37,
+}
+
+
+def _call_subprocess(args: list[str]) -> int:
+    try:
+        return subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True).returncode
+    except subprocess.CalledProcessError as e:
+        print(f"An error occurred when executing {args}:", file=sys.stderr)
+        print(e.output.decode("utf-8"), file=sys.stderr)
+        sys.exit(e.returncode)
+
+
+def _echo(text: str) -> None:
+    sys.stdout.write(text + "\n")
+
+
+if WINDOWS:
+    import winreg
+
+    def _get_win_folder_with_ctypes(csidl_name: str) -> str:
+        import ctypes
+
+        csidl_const = {
+            "CSIDL_APPDATA": 26,
+            "CSIDL_COMMON_APPDATA": 35,
+            "CSIDL_LOCAL_APPDATA": 28,
+        }[csidl_name]
+
+        buf = ctypes.create_unicode_buffer(1024)
+        ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
+
+        # Downgrade to short path name if have highbit chars. See
+        # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
+        has_high_char = False
+        for c in buf:
+            if ord(c) > 255:
+                has_high_char = True
+                break
+        if has_high_char:
+            buf2 = ctypes.create_unicode_buffer(1024)
+            if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
+                buf = buf2
+
+        return buf.value
+
+    def _get_win_folder_from_registry(csidl_name: str) -> str:
+        """This is a fallback technique at best. I'm not sure if using the
+        registry for this guarantees us the correct answer for all CSIDL_*
+        names.
+        """
+        shell_folder_name = {
+            "CSIDL_APPDATA": "AppData",
+            "CSIDL_COMMON_APPDATA": "Common AppData",
+            "CSIDL_LOCAL_APPDATA": "Local AppData",
+        }[csidl_name]
+
+        key = winreg.OpenKey(
+            winreg.HKEY_CURRENT_USER,
+            r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders",
+        )
+        dir, _ = winreg.QueryValueEx(key, shell_folder_name)
+        return dir
+
+    try:
+        from ctypes import windll  # noqa: F401
+
+        _get_win_folder = _get_win_folder_with_ctypes
+    except ImportError:
+        _get_win_folder = _get_win_folder_from_registry
+
+    def _remove_path_windows(target: Path) -> None:
+        value = os.path.normcase(target)
+
+        with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:
+            with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as env_key:
+                try:
+                    old_value, type_ = winreg.QueryValueEx(env_key, "PATH")
+                    paths = [os.path.normcase(item) for item in old_value.split(os.pathsep)]
+                    if value not in paths:
+                        return
+
+                    new_value = os.pathsep.join(p for p in paths if p != value)
+                    winreg.SetValueEx(env_key, "PATH", 0, type_, new_value)
+                except FileNotFoundError:
+                    return
+
+
+def _add_to_path(target: Path) -> None:
+    value = os.path.normcase(target)
+
+    if WINDOWS:
+        with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:
+            with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as env_key:
+                try:
+                    old_value, type_ = winreg.QueryValueEx(env_key, "PATH")
+                    if value in [os.path.normcase(item) for item in old_value.split(os.pathsep)]:
+                        return
+                except FileNotFoundError:
+                    old_value, type_ = "", winreg.REG_EXPAND_SZ
+                new_value = os.pathsep.join([old_value, value]) if old_value else value
+                winreg.SetValueEx(env_key, "PATH", 0, type_, new_value)
+
+        _echo(
+            "Post-install: {} is added to PATH env, please restart your terminal "
+            "to take effect".format(colored("green", value))
+        )
+    else:
+        paths = [os.path.normcase(p) for p in os.getenv("PATH", "").split(os.pathsep)]
+        if value in paths:
+            return
+        _echo(
+            "Post-install: Please add {} to PATH by executing:\n    {}".format(
+                colored("green", value),
+                colored("cyan", f"export PATH={value}:$PATH"),
+            )
+        )
+
+
+def support_ansi() -> bool:
+    if WINDOWS:
+        return (
+            os.getenv("ANSICON") is not None
+            or os.getenv("WT_SESSION") is not None
+            or "ON" == os.getenv("ConEmuANSI")
+            or "xterm" == os.getenv("Term")
+        )
+
+    if not hasattr(sys.stdout, "fileno"):
+        return False
+
+    try:
+        return os.isatty(sys.stdout.fileno())
+    except io.UnsupportedOperation:
+        return False
+
+
+def colored(color: str, text: str, bold: bool = False) -> str:
+    if not support_ansi():
+        return text
+    codes = [FOREGROUND_COLORS[color]]
+    if bold:
+        codes.append(1)
+
+    return "\x1b[{}m{}\x1b[0m".format(";".join(map(str, codes)), text)
+
+
+@dataclasses.dataclass
+class Installer:
+    location: str | None = None
+    version: str | None = None
+    prerelease: bool = False
+    additional_deps: Sequence[str] = ()
+    skip_add_to_path: bool = False
+    output_path: str | None = None
+
+    def __post_init__(self):
+        self._path = self._decide_path()
+        self._path.mkdir(parents=True, exist_ok=True)
+        if self.version is None:
+            self.version = self._get_latest_version()
+
+    def _get_latest_version(self) -> str:
+        resp = urllib.request.urlopen(JSON_URL)
+        metadata = json.load(resp)
+
+        def version_okay(v: str) -> bool:
+            return self.prerelease or all(p.isdigit() for p in v.split("."))
+
+        def sort_version(v: str) -> tuple:
+            parts = []
+            for part in v.split("."):
+                if part.isdigit():
+                    parts.append(int(part))
+                else:
+                    digit, rest = re.match(r"^(\d*)(.*)", part).groups()
+                    if digit:
+                        parts.append(int(digit))
+                    parts.append(rest)
+            return tuple(parts)
+
+        installable_versions = {
+            k for k, v in metadata["releases"].items() if version_okay(k) and not v[0].get("yanked")
+        }
+        releases = sorted(installable_versions, key=sort_version, reverse=True)
+
+        return releases[0]
+
+    def _decide_path(self) -> Path:
+        if self.location is not None:
+            return Path(self.location).expanduser().resolve()
+
+        if WINDOWS:
+            const = "CSIDL_APPDATA"
+            path = os.path.normpath(_get_win_folder(const))
+            path = os.path.join(path, "pdm")
+        elif MACOS:
+            path = os.path.expanduser("~/Library/Application Support/pdm")
+        else:
+            path = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
+            path = os.path.join(path, "pdm")
+
+        return Path(path)
+
+    def _make_env(self) -> Path:
+        venv_path = self._path / "venv"
+
+        _echo(
+            "Installing {} ({}): {}".format(
+                colored("green", "PDM", bold=True),
+                colored("yellow", self.version),
+                colored("cyan", "Creating virtual environment"),
+            )
+        )
+
+        try:
+            import venv
+
+            venv.create(venv_path, clear=False, with_pip=True)
+        except (ModuleNotFoundError, subprocess.CalledProcessError):
+            try:
+                import virtualenv
+            except ModuleNotFoundError:
+                python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
+                url = f"https://bootstrap.pypa.io/virtualenv/{python_version}/virtualenv.pyz"
+                with TemporaryDirectory(prefix="pdm-installer-") as tempdir:
+                    virtualenv_zip = Path(tempdir) / "virtualenv.pyz"
+                    urllib.request.urlretrieve(url, virtualenv_zip)
+                    _call_subprocess([sys.executable, str(virtualenv_zip), str(venv_path)])
+            else:
+                virtualenv.cli_run([str(venv_path)])
+
+        return venv_path
+
+    def _install(self, venv_path: Path) -> None:
+        _echo(
+            "Installing {} ({}): {}".format(
+                colored("green", "PDM", bold=True),
+                colored("yellow", self.version),
+                colored("cyan", "Installing PDM and dependencies"),
+            )
+        )
+
+        if WINDOWS:
+            venv_python = venv_path / "Scripts/python.exe"
+        else:
+            venv_python = venv_path / "bin/python"
+
+        # Re-install the venv pip to ensure it's not DEBUNDLED
+        # See issue/685
+        try:
+            _call_subprocess([str(venv_python), "-m", "ensurepip"])
+        except SystemExit:
+            pass
+        _call_subprocess([str(venv_python), "-m", "pip", "install", "-IU", "pip"])
+
+        if self.version:
+            if self.version.upper() == "HEAD":
+                req = f"git+{REPO}.git@main#egg=pdm"
+            else:
+                req = f"pdm=={self.version}"
+        else:
+            req = "pdm"
+        args = [req] + [d for d in self.additional_deps if d]
+        pip_cmd = [str(venv_python), "-Im", "pip", "install", *args]
+        _call_subprocess(pip_cmd)
+
+    def _make_bin(self, venv_path: Path) -> Path:
+        if self.location:
+            bin_path = self._path / "bin"
+        else:
+            userbase = Path(site.getuserbase())
+            bin_path = userbase / ("Scripts" if WINDOWS else "bin")
+
+        _echo(
+            "Installing {} ({}): {} {}".format(
+                colored("green", "PDM", bold=True),
+                colored("yellow", self.version),
+                colored("cyan", "Making binary at"),
+                colored("green", str(bin_path)),
+            )
+        )
+        bin_path.mkdir(parents=True, exist_ok=True)
+        if WINDOWS:
+            script = bin_path / "pdm.exe"
+            target = venv_path / "Scripts" / "pdm.exe"
+        else:
+            script = bin_path / "pdm"
+            target = venv_path / "bin" / "pdm"
+
+        if script.exists():
+            script.unlink()
+        try:
+            script.symlink_to(target)
+        except OSError:
+            shutil.copy(target, script)
+        return bin_path
+
+    def _post_install(self, venv_path: Path, bin_path: Path) -> None:
+        if WINDOWS:
+            script = bin_path / "pdm.exe"
+        else:
+            script = bin_path / "pdm"
+        subprocess.check_call([str(script), "--help"])
+        print()
+        _echo(
+            "Successfully installed: {} ({}) at {}".format(
+                colored("green", "PDM", bold=True),
+                colored("yellow", self.version),
+                colored("cyan", str(script)),
+            )
+        )
+        if not self.skip_add_to_path:
+            _add_to_path(bin_path)
+        self._write_output(venv_path, script)
+
+    def _write_output(self, venv_path: Path, script: Path) -> None:
+        if not self.output_path:
+            return
+        print("Writing output to", colored("green", self.output_path))
+        output = {
+            "pdm_version": self.version,
+            "pdm_bin": str(script),
+            "install_python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
+            "install_location": str(venv_path),
+        }
+        with open(self.output_path, "w") as f:
+            json.dump(output, f, indent=2)
+
+    def install(self) -> None:
+        venv = self._make_env()
+        self._install(venv)
+        bin_dir = self._make_bin(venv)
+        self._post_install(venv, bin_dir)
+
+    def uninstall(self) -> None:
+        _echo(
+            "Uninstalling {}: {}".format(
+                colored("green", "PDM", bold=True),
+                colored("cyan", "Removing venv and script"),
+            )
+        )
+        if self.location:
+            bin_path = self._path / "bin"
+        else:
+            userbase = Path(site.getuserbase())
+            bin_path = userbase / ("Scripts" if WINDOWS else "bin")
+
+        if WINDOWS:
+            script = bin_path / "pdm.exe"
+        else:
+            script = bin_path / "pdm"
+
+        shutil.rmtree(self._path / "venv")
+        script.unlink()
+
+        if WINDOWS:
+            _remove_path_windows(bin_path)
+
+        print()
+        _echo("Successfully uninstalled")
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-v",
+        "--version",
+        help="Specify the version to be installed, or HEAD to install from the main branch",
+        default=os.getenv("PDM_VERSION"),
+    )
+    parser.add_argument(
+        "--prerelease",
+        action="store_true",
+        help="Allow prereleases to be installed",
+        default=os.getenv("PDM_PRERELEASE"),
+    )
+    parser.add_argument(
+        "--remove",
+        action="store_true",
+        help="Remove the PDM installation",
+        default=os.getenv("PDM_REMOVE"),
+    )
+    parser.add_argument(
+        "-p",
+        "--path",
+        help="Specify the location to install PDM",
+        default=os.getenv("PDM_HOME"),
+    )
+    parser.add_argument(
+        "-d",
+        "--dep",
+        action="append",
+        default=os.getenv("PDM_DEPS", "").split(","),
+        help="Specify additional dependencies, can be given multiple times",
+    )
+    parser.add_argument(
+        "--skip-add-to-path",
+        action="store_true",
+        help="Do not add binary to the PATH.",
+        default=os.getenv("PDM_SKIP_ADD_TO_PATH"),
+    )
+    parser.add_argument("-o", "--output", help="Output file to write the installation info to")
+
+    options = parser.parse_args()
+    installer = Installer(
+        location=options.path,
+        version=options.version,
+        prerelease=options.prerelease,
+        additional_deps=options.dep,
+        skip_add_to_path=options.skip_add_to_path,
+        output_path=options.output,
+    )
+    if options.remove:
+        installer.uninstall()
+    else:
+        installer.install()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/docs/docs/install-pdm.py.sha256 b/docs/docs/install-pdm.py.sha256
new file mode 100644
index 00000000..1f07cbd1
--- /dev/null
+++ b/docs/docs/install-pdm.py.sha256
@@ -0,0 +1,1 @@
+965900c551b4d1ba5127ecf95c127c99b5a1cccef4c183a5ecf447e876fa3160  install-pdm.py
diff --git a/news/2012.feature.md b/news/2012.feature.md
new file mode 100644
index 00000000..f13ca356
--- /dev/null
+++ b/news/2012.feature.md
@@ -0,0 +1,1 @@
+Replace the `DeprecationWarning` with `FutureWarning` for better exposure.
diff --git a/news/2013.bugfix.md b/news/2013.bugfix.md
new file mode 100644
index 00000000..c93f8e8b
--- /dev/null
+++ b/news/2013.bugfix.md
@@ -0,0 +1,1 @@
+Fix the breaking change by adding the functions back to the old location with deprecation warnings.
diff --git a/news/2018.bugfix.md b/news/2018.bugfix.md
new file mode 100644
index 00000000..4c16e931
--- /dev/null
+++ b/news/2018.bugfix.md
@@ -0,0 +1,1 @@
+Fix the duplicate entries in the output of `pdm self list`.
diff --git a/news/2019.bugfix.md b/news/2019.bugfix.md
new file mode 100644
index 00000000..547ad4b3
--- /dev/null
+++ b/news/2019.bugfix.md
@@ -0,0 +1,1 @@
+Disable hashes caching for local files.
diff --git a/news/2026.feature.md b/news/2026.feature.md
new file mode 100644
index 00000000..2c20082f
--- /dev/null
+++ b/news/2026.feature.md
@@ -0,0 +1,1 @@
+Serve `install-pdm.py` and its checksum file on the docs site.
diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py
index ab85384f..b9b52737 100644
--- a/src/pdm/cli/actions.py
+++ b/src/pdm/cli/actions.py
@@ -8,7 +8,7 @@ import os
 import sys
 import textwrap
 import warnings
-from typing import Collection, Iterable, cast
+from typing import Any, Collection, Iterable, cast
 
 from resolvelib.reporters import BaseReporter
 from resolvelib.resolvers import ResolutionImpossible, ResolutionTooDeep, Resolver
@@ -31,6 +31,7 @@ from pdm.models.candidates import Candidate
 from pdm.models.requirements import Requirement, parse_requirement
 from pdm.project import Project
 from pdm.resolver import resolve
+from pdm.utils import deprecation_warning
 
 
 def do_lock(
@@ -351,3 +352,54 @@ def check_update(project: Project) -> None:
         " to disable the check.",
     ]
     project.core.ui.echo("".join(message), err=True, style="info")
+
+
+# Moved functions
+def do_add(*args: Any, **kwargs: Any) -> None:  # pragma: no cover
+    from pdm.cli.commands.add import Command as AddCommand
+
+    deprecation_warning(
+        "`pdm.actions.do_add` has been moved to `pdm.cli.commands.add:Command.do_add` method, "
+        "This function will be removed in the future."
+    )
+    AddCommand().do_add(*args, **kwargs)
+
+
+def do_update(*args: Any, **kwargs: Any) -> None:  # pragma: no cover
+    from pdm.cli.commands.update import Command as UpdateCommand
+
+    deprecation_warning(
+        "`pdm.actions.do_update` has been moved to `pdm.cli.commands.update:Command.do_update` method, "
+        "This function will be removed in the future."
+    )
+    UpdateCommand().do_update(*args, **kwargs)
+
+
+def do_use(*args: Any, **kwargs: Any) -> None:  # pragma: no cover
+    from pdm.cli.commands.use import Command as UseCommand
+
+    deprecation_warning(
+        "`pdm.actions.do_use` has been moved to `pdm.cli.commands.use:Command.do_use` method, "
+        "This function will be removed in the future."
+    )
+    UseCommand().do_use(*args, **kwargs)
+
+
+def do_remove(*args: Any, **kwargs: Any) -> None:  # pragma: no cover
+    from pdm.cli.commands.remove import Command as RemoveCommand
+
+    deprecation_warning(
+        "`pdm.actions.do_remove` has been moved to `pdm.cli.commands.remove:Command.do_remove` method, "
+        "This function will be removed in the future."
+    )
+    RemoveCommand().do_remove(*args, **kwargs)
+
+
+def do_import(*args: Any, **kwargs: Any) -> None:  # pragma: no cover
+    from pdm.cli.commands.import_cmd import Command as ImportCommand
+
+    deprecation_warning(
+        "`pdm.actions.do_import` has been moved to `pdm.cli.commands.import_:Command.do_import` method, "
+        "This function will be removed in the future."
+    )
+    ImportCommand().do_import(*args, **kwargs)
diff --git a/src/pdm/cli/commands/self_cmd.py b/src/pdm/cli/commands/self_cmd.py
index a51e60a4..11d51905 100644
--- a/src/pdm/cli/commands/self_cmd.py
+++ b/src/pdm/cli/commands/self_cmd.py
@@ -13,7 +13,7 @@ from pdm.cli.actions import get_latest_pdm_version_from_pypi
 from pdm.cli.commands.base import BaseCommand
 from pdm.cli.options import verbose_option
 from pdm.cli.utils import Package, build_dependency_graph
-from pdm.compat import Distribution, importlib_metadata
+from pdm.compat import Distribution
 from pdm.environments import BareEnvironment
 from pdm.models.working_set import WorkingSet
 from pdm.project import Project
@@ -23,7 +23,8 @@ PDM_REPO = "https://github.com/pdm-project/pdm"
 
 
 def _get_distributions() -> Iterable[Distribution]:
-    return importlib_metadata.distributions()
+    working_set = WorkingSet()
+    return working_set.values()
 
 
 def list_distributions(plugin_only: bool = False) -> list[Distribution]:
diff --git a/src/pdm/models/caches.py b/src/pdm/models/caches.py
index b763aa99..502cdc0f 100644
--- a/src/pdm/models/caches.py
+++ b/src/pdm/models/caches.py
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, BinaryIO, Generic, Iterable, TypeVar, cast
 from cachecontrol.cache import SeparateBodyBaseCache
 from cachecontrol.caches import FileCache
 from packaging.utils import canonicalize_name, parse_wheel_filename
+
 from pdm._types import CandidateInfo
 from pdm.exceptions import PdmException
 from pdm.models.candidates import Candidate
@@ -128,6 +129,11 @@ class HashCache:
             h.update(chunk)
         return ":".join([h.name, h.hexdigest()])
 
+    def _should_cache(self, link: Link) -> bool:
+        # For now, we only disable caching for local files.
+        # We may add more when we know better about it.
+        return not link.is_file
+
     def get_hash(self, link: Link, session: Session) -> str:
         # If there is no link hash (i.e., md5, sha256, etc.), we don't want
         # to store it.
@@ -142,7 +148,8 @@ class HashCache:
                 hash_value = f"{link.hash_name}:{link.hash}"
             else:
                 hash_value = self._get_file_hash(link, session)
-            self.set(link.url_without_fragment, hash_value)
+            if self._should_cache(link):
+                self.set(link.url_without_fragment, hash_value)
         return hash_value
 
     def _get_path_for_key(self, key: str) -> Path:
diff --git a/src/pdm/models/candidates.py b/src/pdm/models/candidates.py
index 656dfd06..e8b34830 100644
--- a/src/pdm/models/candidates.py
+++ b/src/pdm/models/candidates.py
@@ -481,7 +481,7 @@ class PreparedCandidate:
             if isinstance(medata_hash, dict):
                 hash_name, hash_value = next(iter(medata_hash.items()))
                 if hashlib.new(hash_name, resp.content).hexdigest() != hash_value:
-                    termui.logger.warn("Metadata hash mismatch for %s, ignoring the metadata", link)
+                    termui.logger.warning("Metadata hash mismatch for %s, ignoring the metadata", link)
                     return None
             return MetadataDistribution(resp.text)
 
@@ -497,7 +497,7 @@ class PreparedCandidate:
         pyproject = PyProject(pyproject_toml, ui=self.environment.project.core.ui)
         metadata = pyproject.metadata.unwrap()
         if not metadata:
-            termui.logger.warn("Failed to parse pyproject.toml")
+            termui.logger.warning("Failed to parse pyproject.toml")
             return None
 
         dynamic_fields = metadata.get("dynamic", [])
@@ -542,12 +542,12 @@ class PreparedCandidate:
             termui.logger.info("Running PEP 517 backend to get metadata for %s", self.link)
             self._metadata_dir = builder(source_dir, self.environment).prepare_metadata(metadata_parent)
         except BuildError:
-            termui.logger.warn("Failed to build package, try parsing project files.")
+            termui.logger.warning("Failed to build package, try parsing project files.")
             try:
                 setup = Setup.from_directory(source_dir)
             except Exception:
                 message = "Failed to parse the project files, dependencies may be missing"
-                termui.logger.warn(message)
+                termui.logger.warning(message)
                 warnings.warn(message, RuntimeWarning, stacklevel=1)
                 setup = Setup()
             return setup.as_dist()
diff --git a/src/pdm/utils.py b/src/pdm/utils.py
index 73d51589..3c543c13 100644
--- a/src/pdm/utils.py
+++ b/src/pdm/utils.py
@@ -413,8 +413,8 @@ def deprecation_warning(message: str, stacklevel: int = 1, raise_since: str | No
 
     if raise_since is not None:
         if Version(__version__) >= Version(raise_since):
-            raise DeprecationWarning(message)
-    warnings.warn(message, DeprecationWarning, stacklevel=stacklevel + 1)
+            raise FutureWarning(message)
+    warnings.warn(message, FutureWarning, stacklevel=stacklevel + 1)
 
 
 def is_pip_compatible_with_python(python_version: Version | str) -> bool:


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 79052c5b..218a3205 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -138,10 +138,10 @@ def test_prod_should_not_be_with_dev(project):
 
 
 def test_deprecation_warning():
-    with pytest.warns(DeprecationWarning) as record:
+    with pytest.warns(FutureWarning) as record:
         utils.deprecation_warning("Test warning", raise_since="99.99")
     assert len(record) == 1
     assert str(record[0].message) == "Test warning"
 
-    with pytest.raises(DeprecationWarning):
+    with pytest.raises(FutureWarning):
         utils.deprecation_warning("Test warning", raise_since="0.0")