Skip to content

Commit

Permalink
Merge pull request #126 from callowayproject/sample-config
Browse files Browse the repository at this point in the history
Added sample-config feature
  • Loading branch information
coordt authored Jan 22, 2024
2 parents 2aa2b36 + c15b23b commit 0555ad2
Show file tree
Hide file tree
Showing 47 changed files with 2,845 additions and 1,355 deletions.
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ All types of contributions are encouraged and valued. See the [Table of Contents
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)


Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ release-version: get-version do-release ## Release a specific version: release-

docs: ## generate Sphinx HTML documentation, including API docs
mkdir -p docs
rm -rf docsrc/_autosummary
ls -A1 docs | xargs -I {} rm -rf docs/{}
$(MAKE) -C docsrc clean html
cp -a docsrc/_build/html/. docs
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ Example output:
`-h, --help`
Print help and exit

## Using bumpversion in a script
## Using bump-my-version in a script

If you need to use the version generated by bumpversion in a script, you can make use of the `show` subcommand.
If you need to use the version generated by bump-my-version in a script, you can make use of the `show` subcommand.

Say, for example, that you are using git-flow to manage your project and want to automatically create a release. When you issue `git flow release start` you need to know the new version before applying the change.

Expand Down
55 changes: 54 additions & 1 deletion bumpversion/cli.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
"""bump-my-version Command line interface."""
from pathlib import Path
from typing import List, Optional

import questionary
import rich_click as click
from click.core import Context
from tomlkit import dumps

from bumpversion import __version__
from bumpversion.aliases import AliasedGroup
from bumpversion.bump import do_bump
from bumpversion.config import get_configuration
from bumpversion.config.create import create_configuration
from bumpversion.config.files import find_config_file
from bumpversion.files import ConfiguredFile, modify_files
from bumpversion.show import do_show, log_list
from bumpversion.ui import get_indented_logger, print_warning, setup_logging
from bumpversion.ui import get_indented_logger, print_info, print_warning, setup_logging
from bumpversion.utils import get_context, get_overrides
from bumpversion.visualize import visualize

logger = get_indented_logger(__name__)

Expand Down Expand Up @@ -516,3 +521,51 @@ def replace(
ctx = get_context(config, version, next_version)

modify_files(configured_files, version, next_version, ctx, dry_run)


@cli.command()
@click.option(
"--prompt/--no-prompt",
default=True,
help="Ask the user questions about the configuration.",
)
@click.option(
"--destination",
default="stdout",
help="Where to write the sample configuration.",
type=click.Choice(["stdout", ".bumpversion.toml", "pyproject.toml"]),
)
def sample_config(prompt: bool, destination: str) -> None:
"""Print a sample configuration file."""
if prompt:
destination = questionary.select(
"Destination", choices=["stdout", ".bumpversion.toml", "pyproject.toml"], default=destination
).ask()

destination_config = create_configuration(destination, prompt)

if destination == "stdout":
print_info(dumps(destination_config))
else:
Path(destination).write_text(dumps(destination_config))


@cli.command()
@click.argument("version", nargs=1, type=str, required=False, default="")
@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("--ascii", is_flag=True, help="Use ASCII characters only.")
def show_bump(version: str, config_file: Optional[str], ascii: bool) -> None:
"""Show the possible versions resulting from the bump subcommand."""
found_config_file = find_config_file(config_file)
config = get_configuration(found_config_file)
if not version:
version = config.current_version
box_style = "ascii" if ascii else "light"
visualize(config=config, version_str=version, box_style=box_style)
76 changes: 76 additions & 0 deletions bumpversion/config/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Module for creating a new config file."""
from pathlib import Path
from typing import Tuple

import questionary
from tomlkit import TOMLDocument


def create_configuration(destination: str, prompt: bool) -> TOMLDocument:
"""
Create a new configuration as a TOMLDocument.
Args:
destination: `stdout` or a path to a new or existing file.
prompt: `True` if the user should be prompted for input.
Returns:
The TOMLDocument structure with the updated configuration.
"""
config, destination_config = get_defaults_from_dest(destination)

if prompt:
allow_dirty_default = "(Y/n)" if config["allow_dirty"] else "(y/N)"
answers = questionary.form(
current_version=questionary.text("What is the current version?", default=config["current_version"]),
commit=questionary.confirm(
"Commit changes made when bumping to version control?", default=config["commit"]
),
allow_dirty=questionary.confirm(
"Allow dirty working directory when bumping?",
default=config["allow_dirty"],
instruction=(
"If you are also creating or modifying other files (e.g. a CHANGELOG), say Yes. "
f"{allow_dirty_default} "
),
),
tag=questionary.confirm("Tag changes made when bumping in version control?", default=config["tag"]),
commit_args=questionary.text(
"Any extra arguments to pass to the commit command?",
default=config["commit_args"] or "",
instruction="For example, `--no-verify` is useful if you have a pre-commit hook. ",
),
).ask()
config.update(answers)

for key, val in config.items():
destination_config["tool"]["bumpversion"][key] = val if val is not None else ""

return destination_config


def get_defaults_from_dest(destination: str) -> Tuple[dict, TOMLDocument]:
"""Get the default configuration and the configuration from the destination."""
from tomlkit import document, parse

from bumpversion.config import DEFAULTS

config = DEFAULTS.copy()
if Path(destination).exists():
destination_config = parse(Path(destination).read_text())
else:
destination_config = document()

destination_config.setdefault("tool", {})
destination_config["tool"].setdefault("bumpversion", {})
existing_config = destination_config["tool"]["bumpversion"]
if existing_config:
config.update(existing_config)

project_config = destination_config.get("project", {}).get("version")
config["current_version"] = config["current_version"] or project_config or "0.1.0"
del config["scm_info"]
del config["parts"]
del config["files"]

return config, destination_config
2 changes: 1 addition & 1 deletion bumpversion/scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SCMInfo:

tool: Optional[Type["SourceCodeManager"]] = None
commit_sha: Optional[str] = None
distance_to_latest_tag: Optional[int] = None
distance_to_latest_tag: int = 0
current_version: Optional[str] = None
branch_name: Optional[str] = None
short_branch_name: Optional[str] = None
Expand Down
30 changes: 21 additions & 9 deletions bumpversion/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""General utilities."""
import datetime
import string
from collections import ChainMap
from dataclasses import asdict
from typing import TYPE_CHECKING, Any, List, Optional, Tuple

if TYPE_CHECKING: # pragma: no-coverage
from bumpversion.config import Config
from bumpversion.scm import SCMInfo
from bumpversion.version_part import Version


Expand Down Expand Up @@ -51,19 +53,29 @@ def labels_for_format(serialize_format: str) -> List[str]:
return [item[1] for item in string.Formatter().parse(serialize_format) if item[1]]


def get_context(
config: "Config", current_version: Optional["Version"] = None, new_version: Optional["Version"] = None
) -> ChainMap:
"""Return the context for rendering messages and tags."""
import datetime
def base_context(scm_info: Optional["SCMInfo"] = None) -> ChainMap:
"""The default context for rendering messages and tags."""
from bumpversion.scm import SCMInfo # Including this here to avoid circular imports

scm = asdict(scm_info) if scm_info else asdict(SCMInfo())

ctx = ChainMap(
{"current_version": config.current_version},
{"now": datetime.datetime.now(), "utcnow": datetime.datetime.utcnow()},
return ChainMap(
{
"now": datetime.datetime.now(),
"utcnow": datetime.datetime.now(datetime.timezone.utc),
},
prefixed_environ(),
asdict(config.scm_info),
scm,
{c: c for c in ("#", ";")},
)


def get_context(
config: "Config", current_version: Optional["Version"] = None, new_version: Optional["Version"] = None
) -> ChainMap:
"""Return the context for rendering messages and tags."""
ctx = base_context(config.scm_info)
ctx.new_child({"current_version": config.current_version})
if current_version:
ctx = ctx.new_child({f"current_{part}": current_version[part].value for part in current_version})
if new_version:
Expand Down
137 changes: 137 additions & 0 deletions bumpversion/visualize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Visualize the bumpversion process."""
from dataclasses import dataclass
from typing import List, Optional

from bumpversion.bump import get_next_version
from bumpversion.config import Config
from bumpversion.exceptions import BumpVersionError
from bumpversion.ui import print_info
from bumpversion.utils import base_context, get_context

BOX_CHARS = {
"ascii": ["+", "+", "+", "+", "+", "+", "+", "+", "-", "|", "+"],
"light": ["╯", "╮", "╭", "╰", "┤", "┴", "┬", "├", "─", "│", "┼"],
}


@dataclass
class Border:
"""A border definition."""

corner_bottom_right: str
corner_top_right: str
corner_top_left: str
corner_bottom_left: str
divider_left: str
divider_up: str
divider_down: str
divider_right: str
line: str
pipe: str
cross: str


def lead_string(version_str: str, border: Border, blank: bool = False) -> str:
"""
Return the first part of a string with the bump character or spaces of the correct amount.
Examples:
>>> lead_string("1.0.0", Border(*BOX_CHARS["light"]))
'1.0.0 ── bump ─'
>>> lead_string("1.0.0", Border(*BOX_CHARS["light"]), blank=True)
' '
Args:
version_str: The string to render as the starting point
border: The border definition to draw the lines
blank: If `True`, return a blank string the same length as the version bump string
Returns:
The version bump string or a blank string
"""
version_bump = f"{version_str} {border.line * 2} bump {border.line}"
return " " * len(version_bump) if blank else version_bump


def connection_str(border: Border, has_next: bool = False, has_previous: bool = False) -> str:
"""
Return the correct connection string based on the next and previous.
Args:
border: The border definition to draw the lines
has_next: If `True`, there is a next line
has_previous: If `True`, there is a previous line
Returns:
A string that connects left-to-right and top-to-bottom based on the next and previous
"""
if has_next and has_previous:
return border.divider_right + border.line
elif has_next:
return border.divider_down + border.line
elif has_previous:
return border.corner_bottom_left + border.line
else:
return border.line * 2


def labeled_line(label: str, border: Border, fit_length: Optional[int] = None) -> str:
"""
Return the version part string with the correct padding.
Args:
label: The label to render
border: The border definition to draw the lines
fit_length: The length to fit the label to
Returns:
A labeled line with leading and trailing spaces
"""
if fit_length is None:
fit_length = len(label)
return f" {label} {border.line * (fit_length - len(label))}{border.line} "


def filter_version_parts(config: Config) -> List[str]:
"""
Return the version parts that are in the configuration.
Args:
config: The configuration to check against
Returns:
The version parts that are in the configuration
"""
version_parts = [part for part in config.version_config.order if not part.startswith("$")]
default_context = base_context(config.scm_info)
return [part for part in version_parts if part not in default_context]


def visualize(config: Config, version_str: str, box_style: str = "light") -> None:
"""Output a visualization of the bump-my-version bump process."""
version = config.version_config.parse(version_str)
version_parts = filter_version_parts(config)
num_parts = len(version_parts)

box_style = box_style if box_style in BOX_CHARS else "light"
border = Border(*BOX_CHARS[box_style])

version_lead = lead_string(version_str, border)
blank_lead = lead_string(version_str, border, blank=True)
version_part_length = max(len(part) for part in version_parts)

for i, part in enumerate(version_parts):
line = [version_lead] if i == 0 else [blank_lead]

try:
next_version = get_next_version(version, config, part, None)
next_version_str = config.version_config.serialize(next_version, get_context(config))
except (BumpVersionError, ValueError) as e:
next_version_str = f"invalid: {e}"

has_next = i < num_parts - 1
has_previous = i > 0
line.append(connection_str(border, has_next=has_next, has_previous=has_previous))
line.append(labeled_line(part, border, version_part_length))
line.append(next_version_str)
print_info("".join(line))
2 changes: 2 additions & 0 deletions docsrc/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
```{include} ../LICENSE
```
Loading

0 comments on commit 0555ad2

Please sign in to comment.