Source code for stouputils.version_pkg
"""
This module provides utility functions for printing package version information
in a structured format, including the main package and its dependencies.
Functions:
- show_version: Print the version of the main package and its dependencies.
"""
# Imports
import sys
from .print import CYAN, GREEN, RESET, YELLOW
# Show version function
[docs]
def show_version(main_package: str = "stouputils", primary_color: str = CYAN, secondary_color: str = GREEN, max_depth: int = 2) -> None:
""" Print the version of the main package and its dependencies.
Used by the "stouputils --version" command.
Args:
main_package (str): Name of the main package to show version for
primary_color (str): Color to use for the primary package name
secondary_color (str): Color to use for the secondary package names
max_depth (int): Maximum depth for dependency tree (<= 2 for flat, >=3 for tree)
"""
# Imports
from importlib.metadata import requires, version
def ver(package_name: str) -> str:
try:
return version(package_name)
except Exception:
return ""
def get_deps(package_name: str) -> list[str]:
""" Get the list of dependency names for a package """
try:
deps: list[str] = requires(package_name) or []
# Remove duplicates while preserving order, then sort
unique_deps: list[str] = list(dict.fromkeys([
dep
.split(">")[0]
.split("<")[0]
.split("=")[0]
.split("[")[0]
.split(";")[0]
.strip()
for dep in deps
]))
return sorted(unique_deps)
except Exception:
return []
def print_tree(package_name: str, prefix: str = "", is_last: bool = True, visited: set[str] | None = None, fully_displayed: set[str] | None = None, depth: int = 0, max_depth: int = 3) -> None:
""" Recursively print the dependency tree """
if visited is None:
visited = set()
if fully_displayed is None:
fully_displayed = set()
# Prevent infinite recursion and limit depth
if package_name in visited or depth > max_depth:
return
visited.add(package_name)
# Get version
v: str = ver(package_name).split("version: ")[-1]
if not v:
return
# Determine the tree characters
connector: str = "└── " if is_last else "├── "
# Check if this package was already fully displayed
already_shown: bool = package_name in fully_displayed
# Print current package
if depth == 0:
print(f"{primary_color}{package_name} {secondary_color}v{v}{RESET}")
else:
if already_shown:
print(f"{prefix}{connector}{primary_color}{package_name} {secondary_color}v{v} {YELLOW}[Already shown ^]{RESET}")
# Still mark as fully displayed even when already shown
fully_displayed.add(package_name)
return
else:
print(f"{prefix}{connector}{primary_color}{package_name} {secondary_color}v{v}{RESET}")
# Get dependencies
deps: list[str] = get_deps(package_name)
# Filter dependencies that will actually be displayed (have a version)
valid_deps: list[str] = [dep for dep in deps if ver(dep)]
# Print dependencies recursively
for i, dep in enumerate(valid_deps):
# Determine if this is the last element to display
is_last_dep: bool = (i == len(valid_deps) - 1)
# Extension is based on whether the CURRENT node is last, not the child
extension: str = " " if is_last else "│ "
new_prefix: str = prefix + extension if depth > 0 else ""
print_tree(dep, new_prefix, is_last_dep, visited.copy(), fully_displayed, depth + 1, max_depth)
# Mark this package as fully displayed (with all its dependencies)
fully_displayed.add(package_name)
# Get Python version header
python_version: str = f" Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} "
if max_depth >= 3:
# Display as tree structure
minimum_separator_length: int = len(python_version) + 10
separator_length: int = minimum_separator_length
python_text_length: int = len(python_version)
left_dashes: int = (separator_length - python_text_length) // 2
right_dashes: int = separator_length - python_text_length - left_dashes
separator_with_python: str = "─" * left_dashes + python_version + "─" * right_dashes
separator: str = "─" * separator_length
print(f"{primary_color}{separator_with_python}{RESET}")
print_tree(main_package, max_depth=max_depth - 1)
print(f"{primary_color}{separator}{RESET}")
else:
# Display as flat list (original behavior)
deps: list[str] = requires(main_package) or []
dep_names: list[str] = sorted([
dep
.split(">")[0]
.split("<")[0]
.split("=")[0]
.split("[")[0]
for dep in deps
])
all_deps: list[tuple[str, str]] = [
(x, ver(x).split("version: ")[-1])
for x in (main_package, *dep_names)
]
all_deps = [pair for pair in all_deps if pair[1]] # Filter out packages with no version found
longest_name_length: int = max(len(name) for name, _ in all_deps)
longest_version_length: int = max(len(ver) for _, ver in all_deps)
minimum_separator_length: int = len(python_version) + 10 # Always at least 5 dashes on each side
separator_length: int = max(minimum_separator_length, longest_name_length + longest_version_length + 4)
python_text_length: int = len(python_version)
left_dashes: int = (separator_length - python_text_length) // 2
right_dashes: int = separator_length - python_text_length - left_dashes
separator_with_python: str = "─" * left_dashes + python_version + "─" * right_dashes
separator: str = "─" * separator_length
for pkg, v in all_deps:
pkg_spacing: str = " " * (longest_name_length - len(pkg))
# Highlight the main package with a different style
if pkg == main_package:
print(f"{primary_color}{separator_with_python}{RESET}")
print(f"{primary_color}{pkg}{pkg_spacing} {secondary_color}v{v}{RESET}")
print(f"{primary_color}{separator}{RESET}")
else:
print(f"{primary_color}{pkg}{pkg_spacing} {secondary_color}v{v}{RESET}")
return
# Show version cli
[docs]
def show_version_cli() -> None:
""" Handle the "stouputils --version" CLI command """
# Determine max depth (flat or tree structure)
max_depth: int = 2 # Flat by default
# Check for tree argument
if "--tree" in sys.argv or "-t" in sys.argv:
# Find position of tree argument
pos: int = sys.argv.index("--tree") if "--tree" in sys.argv else sys.argv.index("-t")
# Check for depth argument
if pos + 1 < len(sys.argv):
try:
max_depth = int(sys.argv[pos + 1])
sys.argv.pop(pos + 1) # Remove depth argument
except ValueError:
pass # Keep default if conversion fails
sys.argv.pop(pos) # Remove the --tree/-t argument
# Handle specific package argument
if len(sys.argv) >= 3 and not sys.argv[2].startswith("-"):
return show_version(sys.argv[2], max_depth=max_depth)
# Else, show default package version
return show_version(max_depth=max_depth)