Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/manpages/wheel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ Commands
``convert``
Convert egg or wininst to wheel

``info``
Show information about a wheel file

``tags``
Change the tags on a wheel file

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Reference Guide
:maxdepth: 2

wheel_convert
wheel_unpack
wheel_info
wheel_pack
wheel_tags
wheel_unpack
67 changes: 67 additions & 0 deletions docs/reference/wheel_info.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
wheel info
==========

Usage
-----

::

wheel info [OPTIONS] <wheel_file>


Description
-----------

Display information about a wheel file without unpacking it.

This command shows comprehensive metadata about a wheel file including:

* Package name, version, and build information
* Wheel format version and generator
* Supported Python versions, ABI, and platform tags
* Package metadata such as summary, author, and license
* Classifiers and dependencies
* File count and total size
* Optional detailed file listing


Options
-------

.. option:: -v, --verbose

Show detailed file listing with individual file sizes.


Examples
--------

Display basic information about a wheel::

$ wheel info example_package-1.0-py3-none-any.whl
Name: example-package
Version: 1.0
Wheel-Version: 1.0
Root-Is-Purelib: true
Tags:
py3-none-any
Generator: bdist_wheel (0.40.0)
Summary: An example package
Author: John Doe
License: MIT
Files: 12
Size: 15,234 bytes

Display detailed information with file listing::

$ wheel info --verbose example_package-1.0-py3-none-any.whl
Name: example-package
Version: 1.0
...

File listing:
example_package/__init__.py 45 bytes
example_package/module.py 1,234 bytes
example_package-1.0.dist-info/METADATA 678 bytes
example_package-1.0.dist-info/WHEEL 123 bytes
example_package-1.0.dist-info/RECORD 456 bytes
28 changes: 28 additions & 0 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,31 @@ To install a wheel file, use pip_::
$ pip install someproject-1.5.0-py2-py3-none.whl

.. _pip: https://pypi.org/project/pip/


Inspecting Wheels
-----------------

To inspect the metadata and contents of a wheel file without installing it,
use the ``wheel info`` command::

$ wheel info someproject-1.5.0-py2-py3-none.whl

This will display information about the wheel including:

* Package name and version
* Supported Python versions and platforms
* Dependencies and other metadata
* File count and total size

For more detailed information including a complete file listing, use the
``--verbose`` flag::

$ wheel info --verbose someproject-1.5.0-py2-py3-none.whl

This is useful for:

* Verifying wheel contents before installation
* Debugging packaging issues
* Understanding wheel structure and metadata
* Checking supported platforms and Python versions
16 changes: 16 additions & 0 deletions src/wheel/_commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def tags_f(args: argparse.Namespace) -> None:
print(name)


def info_f(args: argparse.Namespace) -> None:
from .info import info

try:
info(args.wheelfile, args.verbose)
except FileNotFoundError as e:
raise WheelError(str(e)) from e


def version_f(args: argparse.Namespace) -> None:
from .. import __version__

Expand Down Expand Up @@ -129,6 +138,13 @@ def parser() -> argparse.ArgumentParser:
)
tags_parser.set_defaults(func=tags_f)

info_parser = s.add_parser("info", help="Show information about a wheel file")
info_parser.add_argument("wheelfile", help="Wheel file to show information for")
info_parser.add_argument(
"--verbose", "-v", action="store_true", help="Show detailed file listing"
)
info_parser.set_defaults(func=info_f)

version_parser = s.add_parser("version", help="Print version and exit")
version_parser.set_defaults(func=version_f)

Expand Down
127 changes: 127 additions & 0 deletions src/wheel/_commands/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Display information about wheel files.
"""

from __future__ import annotations

import email.policy
from email.parser import BytesParser
from pathlib import Path

from ..wheelfile import WheelFile


def info(path: str, verbose: bool = False) -> None:
"""Display information about a wheel file.

:param path: The path to the wheel file
:param verbose: Show detailed file listing
"""
wheel_path = Path(path)
if not wheel_path.exists():
raise FileNotFoundError(f"Wheel file not found: {path}")

with WheelFile(path) as wf:
# Extract basic wheel information from filename
parsed = wf.parsed_filename
name = parsed.group("name")
version = parsed.group("ver")
python_tag = parsed.group("pyver")
abi_tag = parsed.group("abi")
platform_tag = parsed.group("plat")
build_tag = parsed.group("build")

print(f"Name: {name}")
print(f"Version: {version}")
if build_tag:
print(f"Build: {build_tag}")

# Read WHEEL metadata
try:
with wf.open(f"{wf.dist_info_path}/WHEEL") as wheel_file:
wheel_metadata = BytesParser(policy=email.policy.compat32).parse(
wheel_file
)

print(
f"Wheel-Version: {wheel_metadata.get('Wheel-Version', 'Unknown')}"
)
print(
f"Root-Is-Purelib: {wheel_metadata.get('Root-Is-Purelib', 'Unknown')}"
)

# Get all tags
tags = wheel_metadata.get_all("Tag", [])
if tags:
print("Tags:")
for tag in sorted(tags): # Sort tags for consistent output
print(f" {tag}")

generator = wheel_metadata.get("Generator")
if generator:
print(f"Generator: {generator}")

except KeyError:
print("Warning: WHEEL metadata file not found")

# Read package METADATA
try:
with wf.open(f"{wf.dist_info_path}/METADATA") as metadata_file:
pkg_metadata = BytesParser(policy=email.policy.compat32).parse(
metadata_file
)

summary = pkg_metadata.get("Summary", "")
if summary and summary != "UNKNOWN":
print(f"Summary: {summary}")

author = pkg_metadata.get("Author", "")
if author and author != "UNKNOWN":
print(f"Author: {author}")

author_email = pkg_metadata.get("Author-email")
if author_email and author_email != "UNKNOWN":
print(f"Author-email: {author_email}")

homepage = pkg_metadata.get("Home-page")
if homepage and homepage != "UNKNOWN":
print(f"Home-page: {homepage}")

license_info = pkg_metadata.get("License")
if license_info and license_info != "UNKNOWN":
print(f"License: {license_info}")

# Show classifiers
classifiers = pkg_metadata.get_all("Classifier", [])
if classifiers:
print("Classifiers:")
for classifier in sorted(
classifiers[:5]
): # Sort and limit to first 5
print(f" {classifier}")
if len(classifiers) > 5:
print(f" ... and {len(classifiers) - 5} more")

# Show dependencies
requires_dist = pkg_metadata.get_all("Requires-Dist", [])
if requires_dist:
print("Requires-Dist:")
for req in sorted(requires_dist): # Sort dependencies
print(f" {req}")

except KeyError:
print("Warning: METADATA file not found")

# File information
file_count = len(wf.filelist)
total_size = sum(zinfo.file_size for zinfo in wf.filelist)

print(f"Files: {file_count}")
print(f"Size: {total_size:,} bytes")

# Show file listing if verbose
if verbose:
print("\nFile listing:")
for zinfo in wf.filelist:
size_str = f"{zinfo.file_size:,}" if zinfo.file_size > 0 else "0"
print(f" {zinfo.filename:60} {size_str:>10} bytes")
88 changes: 88 additions & 0 deletions tests/commands/test_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from __future__ import annotations

import os

from pytest import TempPathFactory

from .util import run_command

THISDIR = os.path.dirname(__file__)
TESTWHEEL_NAME = "test-1.0-py2.py3-none-any.whl"
TESTWHEEL_PATH = os.path.join(THISDIR, "..", "testdata", TESTWHEEL_NAME)


def test_info_basic(tmp_path_factory: TempPathFactory) -> None:
"""Test basic wheel info display."""
output = run_command("info", TESTWHEEL_PATH)

# Check basic package information is displayed
assert "Name: test" in output
assert "Version: 1.0" in output
assert "Wheel-Version: 1.0" in output
assert "Root-Is-Purelib: false" in output

# Check tags are displayed
assert "Tags:" in output
assert "py2-none-any" in output
assert "py3-none-any" in output

# Check metadata is displayed
assert "Summary: Test module" in output
assert "Author: Paul Moore" in output
assert "Author-email: [email protected]" in output
assert "Home-page: http://test.example.com/" in output
assert "License: MIT License" in output

# Check file information
assert "Files: 14" in output
assert "Size: 8,114 bytes" in output


def test_info_verbose(tmp_path_factory: TempPathFactory) -> None:
"""Test verbose wheel info display with file listing."""
output = run_command("info", "--verbose", TESTWHEEL_PATH)

# Check that basic info is still there
assert "Name: test" in output
assert "Version: 1.0" in output

# Check that file listing is included
assert "File listing:" in output
assert "hello/hello.py" in output
assert "hello.pyd" in output
assert "test-1.0.dist-info/METADATA" in output
assert "test-1.0.dist-info/WHEEL" in output
assert "test-1.0.dist-info/RECORD" in output

# Check file sizes are displayed
assert "6,656 bytes" in output # hello.pyd
assert "42 bytes" in output # hello.py


def test_info_nonexistent_file() -> None:
"""Test info command with non-existent wheel file."""
try:
output = run_command("info", "nonexistent.whl", catch_systemexit=False)
assert False, "Expected an error for non-existent file"
except Exception:
# Expected to fail
pass


def test_info_help() -> None:
"""Test info command help."""
output = run_command("info", "--help")

assert "usage: wheel info" in output
assert "Wheel file to show information for" in output
assert "wheelfile" in output
assert "--verbose" in output


def test_info_short_verbose_flag() -> None:
"""Test that -v works as alias for --verbose."""
output = run_command("info", "-v", TESTWHEEL_PATH)

# Should include file listing like --verbose
assert "File listing:" in output
assert "hello/hello.py" in output
Loading