bumpver/src/pycalver/__main__.py

353 lines
11 KiB
Python
Raw Normal View History

2018-09-02 21:48:12 +02:00
#!/usr/bin/env python
# This file is part of the pycalver project
2018-11-04 21:34:53 +01:00
# https://gitlab.com/mbarkhau/pycalver
2018-09-02 21:48:12 +02:00
#
2019-02-14 23:19:18 +01:00
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
2018-09-02 21:48:12 +02:00
# SPDX-License-Identifier: MIT
2018-11-06 21:45:33 +01:00
"""
CLI module for PyCalVer.
2018-11-06 21:45:33 +01:00
2018-12-21 23:48:02 +01:00
Provided subcommands: show, test, init, bump
2018-11-06 21:45:33 +01:00
"""
2018-09-02 21:48:12 +02:00
import os
2018-09-02 21:48:12 +02:00
import sys
import click
import logging
2018-11-11 15:09:12 +01:00
import typing as typ
2018-09-02 21:48:12 +02:00
from . import vcs
from . import config
from . import version
from . import rewrite
2018-11-11 15:09:12 +01:00
_VERBOSE = 0
2018-09-02 21:48:12 +02:00
# To enable pretty tracebacks:
# echo "export ENABLE_BACKTRACE=1;" >> ~/.bashrc
2019-02-22 22:47:44 +01:00
if os.environ.get('ENABLE_BACKTRACE') == '1':
2019-01-07 17:30:02 +01:00
import backtrace
2019-02-22 22:47:44 +01:00
2019-01-07 17:30:02 +01:00
backtrace.hook(align=True, strip_path=True, enable_on_envvar_only=True)
2018-12-09 17:03:51 +01:00
click.disable_unicode_literals_warning = True
VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final")
2018-11-11 15:09:12 +01:00
log = logging.getLogger("pycalver.cli")
2018-09-02 21:48:12 +02:00
2019-03-19 19:46:31 +01:00
def _configure_logging(verbose: int = 0) -> None:
2018-11-11 15:09:12 +01:00
if verbose >= 2:
log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-15s - %(message)s"
log_level = logging.DEBUG
elif verbose == 1:
log_format = "%(levelname)-7s - %(message)s"
2018-12-22 00:31:59 +01:00
log_level = logging.INFO
2018-11-11 15:09:12 +01:00
else:
2018-12-20 15:26:48 +01:00
log_format = "%(levelname)-7s - %(message)s"
log_level = logging.INFO
2018-09-02 21:48:12 +02:00
2018-11-11 15:09:12 +01:00
logging.basicConfig(level=log_level, format=log_format, datefmt="%Y-%m-%dT%H:%M:%S")
2019-03-19 19:46:31 +01:00
log.debug("Logging configured.")
2018-09-02 21:48:12 +02:00
2018-12-21 19:19:48 +01:00
def _validate_release_tag(release: str) -> None:
if release in VALID_RELEASE_VALUES:
2018-12-21 19:19:48 +01:00
return
log.error(f"Invalid argument --release={release}")
log.error(f"Valid arguments are: {', '.join(VALID_RELEASE_VALUES)}")
2018-12-21 19:19:48 +01:00
sys.exit(1)
2018-09-02 21:48:12 +02:00
@click.group()
2019-03-24 19:08:31 +01:00
@click.version_option(version="v201903.0028")
2018-12-09 18:06:28 +01:00
@click.help_option()
2018-11-11 15:09:12 +01:00
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
2019-03-19 19:46:31 +01:00
def cli(verbose: int = 0) -> None:
2018-11-11 15:09:12 +01:00
"""Automatically update PyCalVer version strings on python projects."""
global _VERBOSE
_VERBOSE = verbose
@cli.command()
@click.argument("old_version")
@click.argument("pattern", default="{pycalver}")
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
@click.option(
"--release", default=None, metavar="<name>", help="Override release name of current_version"
)
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
@click.option("--minor", is_flag=True, default=False, help="Increment minor component.")
@click.option("--patch", is_flag=True, default=False, help="Increment patch component.")
def test(
old_version: str,
pattern : str = "{pycalver}",
verbose : int = 0,
release : str = None,
major : bool = False,
minor : bool = False,
patch : bool = False,
) -> None:
"""Increment a version number for demo purposes."""
2019-03-19 19:46:31 +01:00
_configure_logging(verbose=max(_VERBOSE, verbose))
2018-12-21 19:19:48 +01:00
if release:
_validate_release_tag(release)
new_version = version.incr(
old_version, pattern=pattern, release=release, major=major, minor=minor, patch=patch
)
if new_version is None:
log.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.")
sys.exit(1)
pep440_version = version.to_pep440(new_version)
print("New Version:", new_version)
print("PEP440 :", pep440_version)
2018-11-11 15:09:12 +01:00
def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config:
try:
_vcs = vcs.get_vcs()
log.debug(f"vcs found: {_vcs.name}")
if fetch:
2018-12-20 15:26:48 +01:00
log.info(f"fetching tags from remote (to turn off use: -n / --no-fetch)")
2018-11-11 15:09:12 +01:00
_vcs.fetch()
version_tags = [tag for tag in _vcs.ls_tags() if version.is_valid(tag, cfg.version_pattern)]
2018-11-11 15:09:12 +01:00
if version_tags:
version_tags.sort(reverse=True)
log.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
2018-12-21 19:22:50 +01:00
latest_version_tag = version_tags[0]
latest_version_pep440 = version.to_pep440(latest_version_tag)
2018-11-11 15:09:12 +01:00
if latest_version_tag > cfg.current_version:
log.info(f"Working dir version : {cfg.current_version}")
log.info(f"Latest version from {_vcs.name:>3} tag: {latest_version_tag}")
2018-12-21 19:22:50 +01:00
cfg = cfg._replace(
current_version=latest_version_tag, pep440_version=latest_version_pep440
)
2018-11-11 15:09:12 +01:00
else:
log.debug("no vcs tags found")
except OSError:
log.debug("No vcs found")
return cfg
2018-09-02 21:48:12 +02:00
@cli.command()
2018-12-09 14:49:13 +01:00
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
@click.option(
2018-12-21 19:19:48 +01:00
"-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin."
2018-12-09 14:49:13 +01:00
)
2018-11-11 15:09:12 +01:00
def show(verbose: int = 0, fetch: bool = True) -> None:
2018-11-06 21:45:33 +01:00
"""Show current version."""
2019-03-19 19:46:31 +01:00
_configure_logging(verbose=max(_VERBOSE, verbose))
ctx: config.ProjectContext = config.init_project_ctx(project_path=".")
cfg: config.MaybeConfig = config.parse(ctx)
2018-09-02 21:48:12 +02:00
if cfg is None:
2018-12-21 19:19:48 +01:00
log.error("Could not parse configuration. Perhaps try 'pycalver init'.")
2018-09-02 23:36:57 +02:00
sys.exit(1)
2018-09-02 21:48:12 +02:00
2018-11-11 15:09:12 +01:00
cfg = _update_cfg_from_vcs(cfg, fetch=fetch)
2018-09-03 22:23:51 +02:00
print(f"Current Version: {cfg.current_version}")
print(f"PEP440 : {cfg.pep440_version}")
2018-09-02 21:48:12 +02:00
@cli.command()
2018-11-11 15:09:12 +01:00
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
2018-09-02 21:48:12 +02:00
@click.option(
2018-11-04 21:11:42 +01:00
"--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files."
2018-09-02 21:48:12 +02:00
)
2018-11-11 15:09:12 +01:00
def init(verbose: int = 0, dry: bool = False) -> None:
2018-11-06 21:45:33 +01:00
"""Initialize [pycalver] configuration."""
2019-03-19 19:46:31 +01:00
_configure_logging(verbose=max(_VERBOSE, verbose))
ctx: config.ProjectContext = config.init_project_ctx(project_path=".")
cfg: config.MaybeConfig = config.parse(ctx)
2018-09-02 21:48:12 +02:00
if cfg:
2019-02-15 20:50:24 +01:00
log.error(f"Configuration already initialized in {ctx.config_filepath}")
2018-09-02 23:36:57 +02:00
sys.exit(1)
2018-09-02 21:48:12 +02:00
if dry:
2019-02-15 20:50:24 +01:00
print(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:")
cfg_text: str = config.default_config(ctx)
print("\n " + "\n ".join(cfg_text.splitlines()))
2019-01-07 17:30:02 +01:00
sys.exit(0)
2018-09-02 21:48:12 +02:00
2018-12-08 19:18:47 +01:00
config.write_content(ctx)
2018-09-02 21:48:12 +02:00
def _assert_not_dirty(_vcs: vcs.VCS, filepaths: typ.Set[str], allow_dirty: bool):
dirty_files = _vcs.status(required_files=filepaths)
2018-11-11 15:09:12 +01:00
if dirty_files:
log.warning(f"{_vcs.name} working directory is not clean. Uncomitted file(s):")
2018-11-11 15:09:12 +01:00
for dirty_file in dirty_files:
log.warning(" " + dirty_file)
2018-11-11 15:09:12 +01:00
if not allow_dirty and dirty_files:
sys.exit(1)
dirty_pattern_files = set(dirty_files) & filepaths
if dirty_pattern_files:
log.error("Not commiting when pattern files are dirty:")
for dirty_file in dirty_pattern_files:
log.warning(" " + dirty_file)
2018-11-11 15:09:12 +01:00
sys.exit(1)
2018-11-15 22:16:16 +01:00
def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> None:
_vcs: typ.Optional[vcs.VCS]
try:
_vcs = vcs.get_vcs()
except OSError:
log.warning("Version Control System not found, aborting commit.")
2018-11-15 22:16:16 +01:00
_vcs = None
filepaths = set(cfg.file_patterns.keys())
if _vcs:
_assert_not_dirty(_vcs, filepaths, allow_dirty)
2019-02-21 16:30:27 +01:00
try:
rewrite.rewrite(new_version, cfg.file_patterns)
except ValueError as ex:
log.error(str(ex))
sys.exit(1)
2018-11-15 22:16:16 +01:00
if _vcs is None or not cfg.commit:
return
for filepath in filepaths:
_vcs.add(filepath)
_vcs.commit(f"bump version to {new_version}")
2018-12-22 00:37:27 +01:00
if cfg.commit and cfg.tag:
2018-11-15 22:16:16 +01:00
_vcs.tag(new_version)
2018-12-22 00:37:27 +01:00
if cfg.commit and cfg.tag and cfg.push:
2018-11-15 22:16:16 +01:00
_vcs.push(new_version)
2019-03-24 19:04:14 +01:00
def _print_diff(cfg: config.Config, new_version: str) -> None:
diff: str = rewrite.diff(new_version, cfg.file_patterns)
if sys.stdout.isatty():
for line in diff.splitlines():
if line.startswith("+++") or line.startswith("---"):
print(line)
elif line.startswith("+"):
print("\u001b[32m" + line + "\u001b[0m")
elif line.startswith("-"):
print("\u001b[31m" + line + "\u001b[0m")
elif line.startswith("@"):
print("\u001b[36m" + line + "\u001b[0m")
else:
print(line)
else:
print(diff)
2018-09-02 21:48:12 +02:00
@cli.command()
2018-12-21 19:19:48 +01:00
@click.option("-v", "--verbose", count=True, help="Control log level. -vv for debug level.")
@click.option(
"-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin."
)
2018-09-02 23:36:57 +02:00
@click.option(
2018-11-11 15:09:12 +01:00
"--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files."
2018-09-02 21:48:12 +02:00
)
2018-09-02 23:36:57 +02:00
@click.option(
2018-12-21 19:19:48 +01:00
"--release",
default=None,
metavar="<name>",
help=(
f"Override release name of current_version. Valid options are: "
f"{', '.join(VALID_RELEASE_VALUES)}."
2018-12-21 19:19:48 +01:00
),
2018-09-02 23:36:57 +02:00
)
2018-09-03 00:14:10 +02:00
@click.option(
"--allow-dirty",
default=False,
is_flag=True,
help=(
"Commit even when working directory is has uncomitted changes. "
"(WARNING: The commit will still be aborted if there are uncomitted "
"to files with version strings."
),
)
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
@click.option("--minor", is_flag=True, default=False, help="Increment minor component.")
@click.option("--patch", is_flag=True, default=False, help="Increment patch component.")
2018-09-03 00:14:10 +02:00
def bump(
2018-11-11 15:09:12 +01:00
release : typ.Optional[str] = None,
2018-12-21 19:19:48 +01:00
verbose : int = 0,
2018-11-11 15:09:12 +01:00
dry : bool = False,
allow_dirty: bool = False,
fetch : bool = True,
major : bool = False,
minor : bool = False,
patch : bool = False,
2018-09-03 00:14:10 +02:00
) -> None:
2018-11-06 21:45:33 +01:00
"""Increment the current version string and update project files."""
2018-11-11 15:40:16 +01:00
verbose = max(_VERBOSE, verbose)
2019-03-19 19:46:31 +01:00
_configure_logging(verbose)
2018-09-02 23:36:57 +02:00
2018-12-21 19:19:48 +01:00
if release:
_validate_release_tag(release)
2018-09-02 21:48:12 +02:00
ctx: config.ProjectContext = config.init_project_ctx(project_path=".")
cfg: config.MaybeConfig = config.parse(ctx)
2018-09-02 21:48:12 +02:00
if cfg is None:
2018-12-21 19:19:48 +01:00
log.error("Could not parse configuration. Perhaps try 'pycalver init'.")
2018-09-02 23:36:57 +02:00
sys.exit(1)
2018-09-02 21:48:12 +02:00
2018-11-11 15:09:12 +01:00
cfg = _update_cfg_from_vcs(cfg, fetch=fetch)
2018-09-03 22:23:51 +02:00
old_version = cfg.current_version
new_version = version.incr(
old_version,
pattern=cfg.version_pattern,
release=release,
major=major,
minor=minor,
patch=patch,
)
if new_version is None:
log.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.")
sys.exit(1)
2018-09-02 21:48:12 +02:00
log.info(f"Old Version: {old_version}")
log.info(f"New Version: {new_version}")
2018-12-22 00:39:20 +01:00
if dry or verbose >= 2:
2019-02-21 16:30:27 +01:00
try:
2019-03-24 19:04:14 +01:00
_print_diff(cfg, new_version)
2019-02-21 16:30:27 +01:00
except ValueError as ex:
log.error(str(ex))
sys.exit(1)
2018-09-02 23:36:57 +02:00
2018-11-15 22:16:16 +01:00
if dry:
2018-09-02 23:36:57 +02:00
return
2018-11-15 22:16:16 +01:00
_bump(cfg, new_version, allow_dirty)
2019-02-22 22:44:23 +01:00
2019-02-22 22:47:44 +01:00
2019-02-22 22:44:23 +01:00
if __name__ == '__main__':
cli()