Repository: pdm-project/pdm (python)

Issue:
On Windows, `pdm run python_file.py` fails
- [X] I have searched the issue tracker and believe that this is not a duplicate.

**Make sure you run commands with `-v` flag before pasting the output.**

## Steps to reproduce

1. Create a simple test file, `test1.py`:

```python
import sys

print("Hello, world")
print(f"This is {sys.executable}")
```

2. Run the file using `pdm run test1.py`

## Actual behavior

The command fails with the following error:

```
❯ pdm run -v test1.py
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\Gustav\.local\bin\pdm.exe\__main__.py", line 7, in <module>
  File "C:\Users\Gustav\.local\pipx\venvs\pdm\Lib\site-packages\pdm\core.py", line 259, in main
    return Core().main(args)
           ^^^^^^^^^^^^^^^^^
  File "C:\Users\Gustav\.local\pipx\venvs\pdm\Lib\site-packages\pdm\core.py", line 191, in main
    raise cast(Exception, err).with_traceback(traceback)
  File "C:\Users\Gustav\.local\pipx\venvs\pdm\Lib\site-packages\pdm\core.py", line 186, in main
    f(project, options)
  File "C:\Users\Gustav\.local\pipx\venvs\pdm\Lib\site-packages\pdm\cli\commands\run.py", line 393, in handle
    exit_code = runner.run(options.command, options.args)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Gustav\.local\pipx\venvs\pdm\Lib\site-packages\pdm\cli\commands\run.py", line 326, in run
    return self._run_process(
           ^^^^^^^^^^^^^^^^^^
  File "C:\Users\Gustav\.local\pipx\venvs\pdm\Lib\site-packages\pdm\cli\commands\run.py", line 225, in _run_process
    process = subprocess.Popen(
              ^^^^^^^^^^^^^^^^^
  File "C:\Users\Gustav\AppData\Local\Programs\Python\Python311\Lib\subprocess.py", line 1022, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "C:\Users\Gustav\AppData\Local\Programs\Python\Python311\Lib\subprocess.py", line 1491, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [WinError 193] %1 is not a valid Win32 application
```

## Expected behavior

The script should be run with the local environment active.

## Environment Information

```bash
# Paste the output of `pdm info && pdm info --env` below:
❯ pdm info; pdm info --env
PDM version:
  2.4.0
Python Interpreter:
  C:\Work\Scratch\Python\.venv\Scripts\python.exe (3.11)
Project Root:
  C:/Work/Scratch/Python
Project Packages:
  None
{
  "implementation_name": "cpython",
  "implementation_version": "3.11.0",
  "os_name": "nt",
  "platform_machine": "AMD64",
  "platform_release": "10",
  "platform_system": "Windows",
  "platform_version": "10.0.22621",
  "python_full_version": "3.11.0",
  "platform_python_implementation": "CPython",
  "python_version": "3.11",
  "sys_platform": "win32"
}
```

Note that this is on Windows, where `.py` files are not directly executable by `CreateProcess`, so the error is accurate, it's the fact that PDM is using `CreateProcess` directly to run the user's command that is the issue here.

## Workaround

It is possible to run the script using `pdm run python test1.py`. This is a viable workaround, but very error prone. Not only is it easy to forget to add `python`, but it's also easy to assume that you can use `py` instead, as that is the normal command to run Python on Windows. Unfortunately, `pdm run py test.py` will *not* run the script with the local environment, but will use the system Python (`py` uses an activated virtual environment, but `pdm run` does not activate the environment).

Pull request:
feat: support implicit python script execution
## Pull Request Check List

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

## Describe what you have changed in this PR.

Close #1626

Patch:
diff --git a/news/1626.feature.md b/news/1626.feature.md
new file mode 100644
index 00000000..6417bca2
--- /dev/null
+++ b/news/1626.feature.md
@@ -0,0 +1,1 @@
+Support running python scripts without prefixing with `python`.
diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py
index 61b83f11..0528cb18 100644
--- a/src/pdm/cli/commands/run.py
+++ b/src/pdm/cli/commands/run.py
@@ -190,12 +190,16 @@ class TaskRunner:
         else:
             assert isinstance(args, Sequence)
             command, *args = args
+            if command.endswith(".py"):
+                args = [command] + args
+                command = str(project.python.executable)
             expanded_command = project_env.which(command)
             if not expanded_command:
                 raise PdmUsageError(
                     f"Command [success]'{command}'[/] is not found in your PATH."
                 )
             expanded_command = os.path.expanduser(os.path.expandvars(expanded_command))
+            real_command = os.path.realpath(expanded_command)
             expanded_args = [
                 os.path.expandvars(arg) for arg in [expanded_command] + args
             ]
@@ -203,9 +207,7 @@ class TaskRunner:
                 not project_env.is_global
                 and not site_packages
                 and (
-                    os.path.basename(os.path.realpath(expanded_command)).startswith(
-                        "python"
-                    )
+                    os.path.basename(real_command).startswith("python")
                     or is_path_relative_to(expanded_command, this_path)
                 )
             ):


Test patch:
diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
index e6beb5a3..eb485810 100644
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -413,7 +413,9 @@ def test_run_show_list_of_scripts(project, invoke):
     assert result_lines[4][1:-1].strip() == "test_shell     │ shell     │ shell command"
 
 
-def test_run_with_another_project_root(project, local_finder, invoke, capfd):
+@pytest.mark.usefixtures("local_finder")
+@pytest.mark.parametrize("explict_python", [True, False])
+def test_run_with_another_project_root(project, invoke, capfd, explict_python):
     project.pyproject.metadata["requires-python"] = ">=3.6"
     project.pyproject.write()
     invoke(["add", "first"], obj=project)
@@ -423,7 +425,10 @@ def test_run_with_another_project_root(project, local_finder, invoke, capfd):
         )
         capfd.readouterr()
         with cd(tmp_dir):
-            ret = invoke(["run", "-p", str(project.root), "python", "main.py"])
+            args = ["run", "-p", str(project.root), "main.py"]
+            if explict_python:
+                args.insert(len(args) - 1, "python")
+            ret = invoke(args)
             out, err = capfd.readouterr()
             assert ret.exit_code == 0, err
             assert out.strip() == "1"