Repository: pallets/click (python)

Issue:
Invalid info_name with shiv and pex
## Summary

When Click is used inside zipapps produced by Shiv or PEX, it displays an invalid "Usage" string. Instead of "Usage: program" it says "Usage: python -m program".

I did some digging and found that this is due to *info_name* being guessed incorrectly in the code introduced by #1609. There is a [test for `__package__` being set to `None`](https://github.com/MLH-Fellowship/click/blob/faaec8294eb819e9f8e6f568ae81b711a861ebbe/src/click/utils.py#L470); however, in the case when the script is run inside Shiv or PEX, `__package__` is instead set to the empty string (""). This is allowed by PEP 366:

> Note that setting __package__ to the empty string explicitly is permitted, and has the effect of disabling all relative imports from that module (since the import machinery will consider it to be a top level module in that case).

## Reproducing

Follow the basic [Shiv hello world example](https://shiv.readthedocs.io/en/latest/#hello-world), but add Click:

_hello.py_:
```python
import click

@click.command()
def main():
    print("Hello world")

if __name__ == "__main__":
    main()
```

_setup.py_:
```python
from setuptools import setup

setup(
    name="hello-world",
    version="0.0.1",
    description="Greet the world.",
    py_modules=["hello"],
    entry_points={
        "console_scripts": ["hello=hello:main"],
    },
    install_requires=['Click==8.1.3'],
)
```

Build the executable zipapps and run them:
```text
$ shiv . -c hello -o hello1.pyz
$ pex . -c hello -o hello2.pyz

$ ./hello1.pyz --help
Usage: python -m hello1 [OPTIONS]

$ ./hello2.pyz foo
Usage: python -m hello2 [OPTIONS]
Try 'python -m hello2 --help' for help.
```

## Expected behavior

Click should not print the Usage string as "python -m hello"; it should just be "hello".

## Environment:

- Python version: 3.10.5
- Click version: 8.1.3
- Shiv version: 1.0.1
- Pex version: 2.1.102

Pull request:
Fix help output for Shiv and PEX
This change ensures that "Usage" help output is correctly displayed when Click scripts are run inside Shiv or PEX zipapps, where `__package__` is set to the empty string rather than `None`.

- fixes #2332
- Problem introduced by #1609

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 9cf54ad..66808a8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -15,6 +15,7 @@ Unreleased
 -   Completion works if there is a dot (``.``) in the program name. :issue:`2166`
 -   Improve type annotations for pyright type checker. :issue:`2268`
 -   Improve responsiveness of ``click.clear()``. :issue:`2284`
+-   Improve command name detection when using Shiv or PEX. :issue:`2332`
 
 
 Version 8.1.3
diff --git a/src/click/utils.py b/src/click/utils.py
index 8c01216..6e74ace 100644
--- a/src/click/utils.py
+++ b/src/click/utils.py
@@ -523,7 +523,8 @@ def _detect_program_name(
     # The value of __package__ indicates how Python was called. It may
     # not exist if a setuptools script is installed as an egg. It may be
     # set incorrectly for entry points created with pip on Windows.
-    if getattr(_main, "__package__", None) is None or (
+    # It is set to "" inside a Shiv or PEX zipapp.
+    if getattr(_main, "__package__", None) in {None, ""} or (
         os.name == "nt"
         and _main.__package__ == ""
         and not os.path.exists(path)


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 5dfeb02..02a6101 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -446,6 +446,7 @@ class MockMain:
         ("example", None, "example"),
         (str(pathlib.Path("example/__main__.py")), "example", "python -m example"),
         (str(pathlib.Path("example/cli.py")), "example", "python -m example.cli"),
+        (str(pathlib.Path("./example")), "", "example"),
     ],
 )
 def test_detect_program_name(path, main, expected):