Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added sample-config feature #126

Merged
merged 8 commits into from
Jan 22, 2024
Merged
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
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 @@
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(

Check warning on line 541 in bumpversion/cli.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/cli.py#L541

Added line #L541 was not covered by tests
"Destination", choices=["stdout", ".bumpversion.toml", "pyproject.toml"], default=destination
).ask()

destination_config = create_configuration(destination, prompt)

Check warning on line 545 in bumpversion/cli.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/cli.py#L545

Added line #L545 was not covered by tests

if destination == "stdout":
print_info(dumps(destination_config))

Check warning on line 548 in bumpversion/cli.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/cli.py#L548

Added line #L548 was not covered by tests
else:
Path(destination).write_text(dumps(destination_config))

Check warning on line 550 in bumpversion/cli.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/cli.py#L550

Added line #L550 was not covered by tests


@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
Loading