Skip to content

Commit 9bce887

Browse files
committed
Added show subcommand.
- 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.
1 parent 72065dc commit 9bce887

12 files changed

+807
-13
lines changed

bumpversion/cli.py

+30-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from bumpversion.bump import do_bump
1111
from bumpversion.config import find_config_file, get_configuration
1212
from bumpversion.logging import setup_logging
13-
from bumpversion.utils import get_context, get_overrides
13+
from bumpversion.show import do_show, log_list
14+
from bumpversion.utils import get_overrides
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -251,15 +252,32 @@ def bump(
251252
do_bump(version_part, new_version, config, found_config_file, dry_run)
252253

253254

254-
def log_list(config: Config, version_part: Optional[str], new_version: Optional[str]) -> None:
255-
"""Output configuration with new version."""
256-
ctx = get_context(config)
257-
if version_part:
258-
version = config.version_config.parse(config.current_version)
259-
next_version = get_next_version(version, config, version_part, new_version)
260-
next_version_str = config.version_config.serialize(next_version, ctx)
261-
262-
click.echo(f"new_version={next_version_str}")
255+
@cli.command()
256+
@click.argument("args", nargs=-1, type=str)
257+
@click.option(
258+
"--config-file",
259+
metavar="FILE",
260+
required=False,
261+
envvar="BUMPVERSION_CONFIG_FILE",
262+
type=click.Path(exists=True),
263+
help="Config file to read most of the variables from.",
264+
)
265+
@click.option(
266+
"-f",
267+
"--format",
268+
"format_",
269+
required=False,
270+
envvar="BUMPVERSION_FORMAT",
271+
type=click.Choice(["default", "yaml", "json"], case_sensitive=False),
272+
default="default",
273+
help="Config file to read most of the variables from.",
274+
)
275+
def show(args: List[str], config_file: Optional[str], format_: str) -> None:
276+
"""Show current configuration information."""
277+
found_config_file = find_config_file(config_file)
278+
config = get_configuration(found_config_file)
263279

264-
for key, value in config.dict(exclude={"scm_info", "parts"}).items():
265-
click.echo(f"{key}={value}")
280+
if not args:
281+
do_show("all", config=config, format_=format_)
282+
else:
283+
do_show(*args, config=config, format_=format_)

bumpversion/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,9 @@ class ConfigurationError(BumpVersionError):
5252
"""A configuration key-value is missing or in the wrong type."""
5353

5454
pass
55+
56+
57+
class BadInputError(BumpVersionError):
58+
"""User input was bad."""
59+
60+
pass

bumpversion/scm.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ class SCMInfo:
2626
current_version: Optional[str] = None
2727
dirty: Optional[bool] = None
2828

29+
def __str__(self):
30+
return self.__repr__()
31+
32+
def __repr__(self):
33+
tool_name = self.tool.__name__ if self.tool else "No SCM tool"
34+
return (
35+
f"SCMInfo(tool={tool_name}, commit_sha={self.commit_sha}, "
36+
f"distance_to_latest_tag={self.distance_to_latest_tag}, current_version={self.current_version}, "
37+
f"dirty={self.dirty})"
38+
)
39+
2940

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

191+
def __str__(self):
192+
return self.__repr__()
193+
194+
def __repr__(self):
195+
return f"{self.__class__.__name__}"
196+
180197

181198
class Git(SourceCodeManager):
182199
"""Git implementation."""
@@ -257,7 +274,7 @@ def tag(cls, name: str, sign: bool = False, message: Optional[str] = None) -> No
257274
Args:
258275
name: The name of the tag
259276
sign: True to sign the tag
260-
message: A optional message to annotate the tag.
277+
message: An optional message to annotate the tag.
261278
"""
262279
command = ["git", "tag", name]
263280
if sign:

bumpversion/show.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Functions for displaying information about the version."""
2+
import dataclasses
3+
from io import StringIO
4+
from pprint import pprint
5+
from typing import Any, Optional
6+
7+
from bumpversion.bump import get_next_version
8+
from bumpversion.config import Config
9+
from bumpversion.exceptions import BadInputError
10+
from bumpversion.ui import print_error, print_info
11+
from bumpversion.utils import get_context
12+
13+
14+
def output_default(value: dict) -> None:
15+
"""Output the value with key=value or just value if there is only one item."""
16+
if len(value) == 1:
17+
print_info(list(value.values())[0])
18+
else:
19+
buffer = StringIO()
20+
pprint(value, stream=buffer) # noqa: T203
21+
print_info(buffer.getvalue())
22+
23+
24+
def output_yaml(value: dict) -> None:
25+
"""Output the value as yaml."""
26+
from bumpversion.yaml_dump import dump
27+
28+
print_info(dump(value))
29+
30+
31+
def output_json(value: dict) -> None:
32+
"""Output the value as json."""
33+
import json
34+
35+
def default_encoder(obj: Any) -> str:
36+
if dataclasses.is_dataclass(obj):
37+
return str(obj)
38+
elif isinstance(obj, type):
39+
return obj.__name__
40+
raise TypeError(f"Object of type {type(obj), str(obj)} is not JSON serializable")
41+
42+
print_info(json.dumps(value, sort_keys=True, indent=2, default=default_encoder))
43+
44+
45+
OUTPUTTERS = {
46+
"yaml": output_yaml,
47+
"json": output_json,
48+
"default": output_default,
49+
}
50+
51+
52+
def resolve_name(obj: Any, name: str, default: Any = None, err_on_missing: bool = False) -> Any:
53+
"""
54+
Get a key or attr ``name`` from obj or default value.
55+
56+
Copied and modified from Django Template variable resolutions
57+
58+
Resolution methods:
59+
60+
- Mapping key lookup
61+
- Attribute lookup
62+
- Sequence index
63+
64+
Args:
65+
obj: The object to access
66+
name: A dotted name to the value, such as ``mykey.0.name``
67+
default: If the name cannot be resolved from the object, return this value
68+
err_on_missing: Raise a `BadInputError` if the name cannot be resolved
69+
70+
Returns:
71+
The value at the resolved name or the default value.
72+
73+
Raises:
74+
BadInputError: If we cannot resolve the name and `err_on_missing` is `True`
75+
76+
# noqa: DAR401
77+
"""
78+
lookups = name.split(".")
79+
current = obj
80+
try: # catch-all for unexpected failures
81+
for bit in lookups:
82+
try: # dictionary lookup
83+
current = current[bit]
84+
# ValueError/IndexError are for numpy.array lookup on
85+
# numpy < 1.9 and 1.9+ respectively
86+
except (TypeError, AttributeError, KeyError, ValueError, IndexError):
87+
try: # attribute lookup
88+
current = getattr(current, bit)
89+
except (TypeError, AttributeError):
90+
# Reraise if the exception was raised by a @property
91+
if bit in dir(current):
92+
raise
93+
try: # list-index lookup
94+
current = current[int(bit)]
95+
except (
96+
IndexError, # list index out of range
97+
ValueError, # invalid literal for int()
98+
KeyError, # current is a dict without `int(bit)` key
99+
TypeError,
100+
): # un-subscript-able object
101+
return default
102+
return current
103+
except Exception as e: # noqa: BLE001 # pragma: no cover
104+
if err_on_missing:
105+
raise BadInputError(f"Could not resolve '{name}'") from e
106+
else:
107+
return default
108+
109+
110+
def log_list(config: Config, version_part: Optional[str], new_version: Optional[str]) -> None:
111+
"""Output configuration with new version."""
112+
ctx = get_context(config)
113+
if version_part:
114+
version = config.version_config.parse(config.current_version)
115+
next_version = get_next_version(version, config, version_part, new_version)
116+
next_version_str = config.version_config.serialize(next_version, ctx)
117+
118+
print_info(f"new_version={next_version_str}")
119+
120+
for key, value in config.dict(exclude={"scm_info", "parts"}).items():
121+
print_info(f"{key}={value}")
122+
123+
124+
def do_show(*args, config: Config, format_: str = "default") -> None:
125+
"""Show current version or configuration information."""
126+
config_dict = config.dict()
127+
128+
try:
129+
if "all" in args or not args:
130+
show_items = config_dict
131+
else:
132+
show_items = {key: resolve_name(config_dict, key) for key in args}
133+
134+
OUTPUTTERS.get(format_, OUTPUTTERS["default"])(show_items)
135+
except BadInputError as e:
136+
print_error(e.message)

bumpversion/ui.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Utilities for user interface."""
2+
from click import UsageError, echo
3+
4+
5+
def print_info(msg: str) -> None:
6+
"""Echo a message to the console."""
7+
echo(msg)
8+
9+
10+
def print_error(msg: str) -> None:
11+
"""Raise an error and exit."""
12+
raise UsageError(msg)

0 commit comments

Comments
 (0)