Skip to content

Commit

Permalink
Added show subcommand.
Browse files Browse the repository at this point in the history
- supersedes the `--list` option
- provides much more capability
- Can output in YAML, JSON, and default
- Can specify one or more items to display
- Can use dotted-notation to pull items from nested data structures.
  • Loading branch information
coordt committed Jun 21, 2023
1 parent 72065dc commit 9bce887
Show file tree
Hide file tree
Showing 12 changed files with 807 additions and 13 deletions.
42 changes: 30 additions & 12 deletions bumpversion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from bumpversion.bump import do_bump
from bumpversion.config import find_config_file, get_configuration
from bumpversion.logging import setup_logging
from bumpversion.utils import get_context, get_overrides
from bumpversion.show import do_show, log_list
from bumpversion.utils import get_overrides

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -251,15 +252,32 @@ def bump(
do_bump(version_part, new_version, config, found_config_file, dry_run)


def log_list(config: Config, version_part: Optional[str], new_version: Optional[str]) -> None:
"""Output configuration with new version."""
ctx = get_context(config)
if version_part:
version = config.version_config.parse(config.current_version)
next_version = get_next_version(version, config, version_part, new_version)
next_version_str = config.version_config.serialize(next_version, ctx)

click.echo(f"new_version={next_version_str}")
@cli.command()
@click.argument("args", nargs=-1, type=str)
@click.option(
"--config-file",
metavar="FILE",
required=False,
envvar="BUMPVERSION_CONFIG_FILE",
type=click.Path(exists=True),
help="Config file to read most of the variables from.",
)
@click.option(
"-f",
"--format",
"format_",
required=False,
envvar="BUMPVERSION_FORMAT",
type=click.Choice(["default", "yaml", "json"], case_sensitive=False),
default="default",
help="Config file to read most of the variables from.",
)
def show(args: List[str], config_file: Optional[str], format_: str) -> None:
"""Show current configuration information."""
found_config_file = find_config_file(config_file)
config = get_configuration(found_config_file)

for key, value in config.dict(exclude={"scm_info", "parts"}).items():
click.echo(f"{key}={value}")
if not args:
do_show("all", config=config, format_=format_)
else:
do_show(*args, config=config, format_=format_)
6 changes: 6 additions & 0 deletions bumpversion/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@ class ConfigurationError(BumpVersionError):
"""A configuration key-value is missing or in the wrong type."""

pass


class BadInputError(BumpVersionError):
"""User input was bad."""

pass
19 changes: 18 additions & 1 deletion bumpversion/scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ class SCMInfo:
current_version: Optional[str] = None
dirty: Optional[bool] = None

def __str__(self):
return self.__repr__()

def __repr__(self):
tool_name = self.tool.__name__ if self.tool else "No SCM tool"
return (
f"SCMInfo(tool={tool_name}, commit_sha={self.commit_sha}, "
f"distance_to_latest_tag={self.distance_to_latest_tag}, current_version={self.current_version}, "
f"dirty={self.dirty})"
)


class SourceCodeManager:
"""Base class for version control systems."""
Expand Down Expand Up @@ -177,6 +188,12 @@ def tag_in_scm(cls, config: "Config", context: MutableMapping, dry_run: bool = F
if do_tag:
cls.tag(tag_name, sign_tags, tag_message)

def __str__(self):
return self.__repr__()

Check warning on line 192 in bumpversion/scm.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/scm.py#L192

Added line #L192 was not covered by tests

def __repr__(self):
return f"{self.__class__.__name__}"

Check warning on line 195 in bumpversion/scm.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/scm.py#L195

Added line #L195 was not covered by tests


class Git(SourceCodeManager):
"""Git implementation."""
Expand Down Expand Up @@ -257,7 +274,7 @@ def tag(cls, name: str, sign: bool = False, message: Optional[str] = None) -> No
Args:
name: The name of the tag
sign: True to sign the tag
message: A optional message to annotate the tag.
message: An optional message to annotate the tag.
"""
command = ["git", "tag", name]
if sign:
Expand Down
136 changes: 136 additions & 0 deletions bumpversion/show.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Functions for displaying information about the version."""
import dataclasses
from io import StringIO
from pprint import pprint
from typing import Any, Optional

from bumpversion.bump import get_next_version
from bumpversion.config import Config
from bumpversion.exceptions import BadInputError
from bumpversion.ui import print_error, print_info
from bumpversion.utils import get_context


def output_default(value: dict) -> None:
"""Output the value with key=value or just value if there is only one item."""
if len(value) == 1:
print_info(list(value.values())[0])
else:
buffer = StringIO()
pprint(value, stream=buffer) # noqa: T203
print_info(buffer.getvalue())


def output_yaml(value: dict) -> None:
"""Output the value as yaml."""
from bumpversion.yaml_dump import dump

print_info(dump(value))


def output_json(value: dict) -> None:
"""Output the value as json."""
import json

def default_encoder(obj: Any) -> str:
if dataclasses.is_dataclass(obj):
return str(obj)
elif isinstance(obj, type):
return obj.__name__
raise TypeError(f"Object of type {type(obj), str(obj)} is not JSON serializable")

Check warning on line 40 in bumpversion/show.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/show.py#L39-L40

Added lines #L39 - L40 were not covered by tests

print_info(json.dumps(value, sort_keys=True, indent=2, default=default_encoder))


OUTPUTTERS = {
"yaml": output_yaml,
"json": output_json,
"default": output_default,
}


def resolve_name(obj: Any, name: str, default: Any = None, err_on_missing: bool = False) -> Any:
"""
Get a key or attr ``name`` from obj or default value.
Copied and modified from Django Template variable resolutions
Resolution methods:
- Mapping key lookup
- Attribute lookup
- Sequence index
Args:
obj: The object to access
name: A dotted name to the value, such as ``mykey.0.name``
default: If the name cannot be resolved from the object, return this value
err_on_missing: Raise a `BadInputError` if the name cannot be resolved
Returns:
The value at the resolved name or the default value.
Raises:
BadInputError: If we cannot resolve the name and `err_on_missing` is `True`
# noqa: DAR401
"""
lookups = name.split(".")
current = obj
try: # catch-all for unexpected failures
for bit in lookups:
try: # dictionary lookup
current = current[bit]
# ValueError/IndexError are for numpy.array lookup on
# numpy < 1.9 and 1.9+ respectively
except (TypeError, AttributeError, KeyError, ValueError, IndexError):
try: # attribute lookup
current = getattr(current, bit)
except (TypeError, AttributeError):
# Reraise if the exception was raised by a @property
if bit in dir(current):
raise
try: # list-index lookup
current = current[int(bit)]
except (
IndexError, # list index out of range
ValueError, # invalid literal for int()
KeyError, # current is a dict without `int(bit)` key
TypeError,
): # un-subscript-able object
return default
return current
except Exception as e: # noqa: BLE001 # pragma: no cover
if err_on_missing:
raise BadInputError(f"Could not resolve '{name}'") from e

Check warning on line 105 in bumpversion/show.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/show.py#L105

Added line #L105 was not covered by tests
else:
return default


def log_list(config: Config, version_part: Optional[str], new_version: Optional[str]) -> None:
"""Output configuration with new version."""
ctx = get_context(config)
if version_part:
version = config.version_config.parse(config.current_version)
next_version = get_next_version(version, config, version_part, new_version)
next_version_str = config.version_config.serialize(next_version, ctx)

print_info(f"new_version={next_version_str}")

for key, value in config.dict(exclude={"scm_info", "parts"}).items():
print_info(f"{key}={value}")


def do_show(*args, config: Config, format_: str = "default") -> None:
"""Show current version or configuration information."""
config_dict = config.dict()

try:
if "all" in args or not args:
show_items = config_dict
else:
show_items = {key: resolve_name(config_dict, key) for key in args}

OUTPUTTERS.get(format_, OUTPUTTERS["default"])(show_items)
except BadInputError as e:
print_error(e.message)

Check warning on line 136 in bumpversion/show.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/show.py#L135-L136

Added lines #L135 - L136 were not covered by tests
12 changes: 12 additions & 0 deletions bumpversion/ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Utilities for user interface."""
from click import UsageError, echo


def print_info(msg: str) -> None:
"""Echo a message to the console."""
echo(msg)


def print_error(msg: str) -> None:
"""Raise an error and exit."""
raise UsageError(msg)

Check warning on line 12 in bumpversion/ui.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/ui.py#L12

Added line #L12 was not covered by tests

0 comments on commit 9bce887

Please sign in to comment.