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

Switch from Typer to Click #35

Merged
merged 4 commits into from
Nov 24, 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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: 'v0.7.4'
rev: 'v0.8.0'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down Expand Up @@ -31,7 +31,6 @@ repos:
- id: fix-byte-order-marker
- id: fix-encoding-pragma
args: ["--remove"]
- id: requirements-txt-fixer
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
Expand All @@ -43,15 +42,16 @@ repos:
hooks:
- id: mypy
args: [--no-strict-optional, --ignore-missing-imports]
additional_dependencies: ["toml", "types-all"]
additional_dependencies: ["toml"]
- repo: https://github.com/jsh9/pydoclint
rev: 0.5.9
hooks:
- id: pydoclint
args:
- "--config=pyproject.toml"
exclude: test.*
- repo: https://github.com/econchick/interrogate
rev: 1.7.0 # or master if you're bold
rev: 1.7.0
hooks:
- id: interrogate
exclude: test.*
5 changes: 1 addition & 4 deletions generate_changelog/_attr_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ def attribute_docstrings(obj: type) -> dict:
nodes = list(ast_class.body)
docstrings = {}

for (
a,
b,
) in pairs(nodes):
for a, b in pairs(nodes):
if isinstance(a, ast.AnnAssign) and isinstance(a.target, ast.Name) and a.simple:
name = a.target.id
elif isinstance(a, ast.Assign) and len(a.targets) == 1 and isinstance(a.targets[0], ast.Name):
Expand Down
7 changes: 3 additions & 4 deletions generate_changelog/actions/file_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from pathlib import Path

import typer
import rich_click as click

from generate_changelog.actions import register_builtin
from generate_changelog.configuration import StrOrCallable
Expand All @@ -30,8 +30,7 @@ def __call__(self, *args, **kwargs) -> str:
filepath.touch()

if not filepath.exists():
typer.echo(f"The file '{filepath}' does not exist.", err=True)
raise typer.Exit(1)
raise click.UsageError(f"The file '{filepath}' does not exist.")

return filepath.read_text() or ""

Expand All @@ -55,7 +54,7 @@ def __call__(self, input_text: StrOrCallable) -> StrOrCallable:
@register_builtin
def stdout(content: str) -> str:
"""Write content to stdout."""
typer.echo(content)
click.echo(content)
return content


Expand Down
5 changes: 4 additions & 1 deletion generate_changelog/actions/matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ class MetadataMatch:
operator: in
value: ["fix", "refactor", "update"]

Valid operators: ``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``, ``is``, ``is not``, ``in``, ``not in``
Valid operators: `==`, `!=`, `<`, `>`, `>=`, `<=`, `is`, `is not`, `in`, `not in`

Attributes:
operator_map: A map of operator names to operators

Args:
attribute: The name of the metadata key whose value will be evaluated
Expand Down
2 changes: 1 addition & 1 deletion generate_changelog/actions/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def bash(script: str, environment: Optional[dict] = None) -> str:

command = ["bash", "--noprofile", "--norc", "-eo", "pipefail", script_path]

result = subprocess.run(
result = subprocess.run( # NOQA: S603
command,
env=environment,
encoding="utf-8",
Expand Down
146 changes: 64 additions & 82 deletions generate_changelog/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,88 +2,79 @@

import functools
import json
from enum import Enum
from pathlib import Path
from typing import Callable, Optional

import typer
import rich_click as click
from click.core import Context, Parameter
from git import Repo

from generate_changelog import __version__
from generate_changelog.commits import get_context_from_tags
from generate_changelog.configuration import DEFAULT_CONFIG_FILE_NAMES, Configuration, write_default_config
from generate_changelog.release_hint import suggest_release_type

app = typer.Typer()


class OutputOption(str, Enum):
"""Types of output available."""

release_hint = "release-hint"
notes = "notes"
all = "all"


def version_callback(value: bool) -> None:
"""Display the version and exit."""
import generate_changelog

if value:
typer.echo(generate_changelog.__version__)
raise typer.Exit()


def generate_config_callback(value: bool) -> None:
def generate_config_callback(ctx: Context, param: Parameter, value: bool) -> None:
"""Generate a default configuration file."""
if not value: # pragma: no cover
return
f = Path.cwd() / Path(DEFAULT_CONFIG_FILE_NAMES[0])
file_path = f.expanduser().resolve()
if file_path.exists():
overwrite = typer.confirm(f"{file_path} already exists. Overwrite it?")
overwrite = click.confirm(f"{file_path} already exists. Overwrite it?")
if not overwrite:
typer.echo("Aborting configuration file generation.")
typer.Abort()
click.echo("Aborting configuration file generation.")
click.Abort()
write_default_config(f)
typer.echo(f"The configuration file was written to {f}.")
raise typer.Exit()


@app.command()
def main(
version: Optional[bool] = typer.Option(
None, "--version", help="Show program's version number and exit", callback=version_callback, is_eager=True
),
generate_config: Optional[bool] = typer.Option(
None,
"--generate-config",
help="Generate a default configuration file",
callback=generate_config_callback,
),
config_file: Optional[Path] = typer.Option(
None, "--config", "-c", help="Path to the config file.", envvar="CHANGELOG_CONFIG_FILE"
),
repository_path: Optional[Path] = typer.Option(
None, "--repo-path", "-r", help="Path to the repository, if not within the current directory"
),
starting_tag: Optional[str] = typer.Option(None, "--starting-tag", "-t", help="Tag to generate a changelog from."),
output: Optional[OutputOption] = typer.Option(None, "--output", "-o", help="What output to generate."),
skip_output_pipeline: bool = typer.Option(
False, "--skip-output-pipeline", help="Do not execute the output pipeline in the configuration."
),
branch_override: Optional[str] = typer.Option(
None, "--branch-override", "-b", help="Override the current branch for release hint decisions."
),
click.echo(f"The configuration file was written to {f}.")
ctx.exit()


@click.command(
context_settings={
"help_option_names": ["-h", "--help"],
},
add_help_option=True,
)
@click.option(
"--generate-config",
is_flag=True,
help="Generate a default configuration file",
callback=generate_config_callback,
is_eager=True,
expose_value=False,
)
@click.option(
"--config",
"-c",
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
help="Path to the config file.",
envvar="CHANGELOG_CONFIG_FILE",
)
@click.option("--repo-path", "-r", help="Path to the repository, if not within the current directory")
@click.option("--starting-tag", "-t", help="Tag to generate a changelog from.")
@click.option("--output", "-o", type=click.Choice(["release-hint", "notes", "all"]), help="What output to generate.")
@click.option("--skip-output-pipeline", is_flag=True, help="Do not execute the output pipeline in the configuration.")
@click.option("--branch-override", "-b", help="Override the current branch for release hint decisions.")
@click.version_option(version=__version__)
def cli(
config: Optional[Path],
repo_path: Optional[Path],
starting_tag: Optional[str],
output: Optional[str],
skip_output_pipeline: bool,
branch_override: Optional[str],
) -> None:
"""Generate a change log from git commits."""
from generate_changelog import templating
from generate_changelog.pipeline import pipeline_factory

echo_func = functools.partial(echo, quiet=bool(output))
config = get_user_config(config_file, echo_func)
configuration = get_user_config(config, echo_func)

if repository_path: # pragma: no cover
repository = Repo(repository_path)
if repo_path: # pragma: no cover
repository = Repo(repo_path)
else:
repository = Repo(search_parent_directories=True)

Expand All @@ -92,45 +83,43 @@
else:
current_branch = repository.active_branch

# get starting tag based configuration if not passed in
if not starting_tag and config.starting_tag_pipeline:
start_tag_pipeline = pipeline_factory(config.starting_tag_pipeline, **config.variables)
# get starting tag based on configuration if not passed in
if not starting_tag and configuration.starting_tag_pipeline:
start_tag_pipeline = pipeline_factory(configuration.starting_tag_pipeline, **configuration.variables)
starting_tag = start_tag_pipeline.run()

if not starting_tag:
echo_func("No starting tag found. Generating entire change log.")
else:
echo_func(f"Generating change log from tag: '{starting_tag}'.")

version_contexts = get_context_from_tags(repository, config, starting_tag)
version_contexts = get_context_from_tags(repository, configuration, starting_tag)

branch_name = branch_override or current_branch.name
release_hint = suggest_release_type(branch_name, version_contexts, config)
release_hint = suggest_release_type(branch_name, version_contexts, configuration)

# use the output pipeline to deal with the rendered change log.
has_starting_tag = bool(starting_tag)
rendered_chglog = templating.render_changelog(version_contexts, config, has_starting_tag)
rendered_chglog = templating.render_changelog(version_contexts, configuration, has_starting_tag)

if not skip_output_pipeline:
echo_func("Executing output pipeline.")
output_pipeline = pipeline_factory(config.output_pipeline, **config.variables)
output_pipeline = pipeline_factory(configuration.output_pipeline, **configuration.variables)
output_pipeline.run(rendered_chglog.full)

if output == OutputOption.release_hint:
typer.echo(release_hint)
elif output == OutputOption.notes:
if output == "release-hint":
click.echo(release_hint)
elif output == "notes":
if rendered_chglog.notes:
typer.echo(rendered_chglog.notes)
click.echo(rendered_chglog.notes)

Check warning on line 114 in generate_changelog/cli.py

View check run for this annotation

Codecov / codecov/patch

generate_changelog/cli.py#L114

Added line #L114 was not covered by tests
else:
typer.echo(rendered_chglog.full)
elif output == OutputOption.all:
click.echo(rendered_chglog.full)
elif output == "all":
notes = rendered_chglog.notes or rendered_chglog.full
out = {"release_hint": release_hint, "notes": notes}
typer.echo(json.dumps(out))
click.echo(json.dumps(out))
else:
typer.echo("Done.")

raise typer.Exit()
click.echo("Done.")


def get_user_config(config_file: Optional[Path], echo_func: Callable) -> Configuration:
Expand Down Expand Up @@ -167,11 +156,4 @@
quiet: Do it quietly
"""
if not quiet:
typer.echo(message)


typer_click_object = typer.main.get_command(app)


if __name__ == "__main__":
app()
click.echo(message)
12 changes: 5 additions & 7 deletions generate_changelog/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from dataclasses import asdict, dataclass, field
from pathlib import Path

import typer
import rich_click as click
from ruamel.yaml import YAML

yaml = YAML()
Expand Down Expand Up @@ -244,17 +244,15 @@ def update_from_file(self, filename: Path) -> None:
filename: Path to the YAML file

Raises:
Exit: if the path does not exist or is a directory
click.UsageError: if the path does not exist or is a directory
"""
file_path = filename.expanduser().resolve()

if not file_path.exists():
typer.echo(f"'{filename}' does not exist.", err=True)
raise typer.Exit(1)
raise click.UsageError(f"'{filename}' does not exist.")

if not file_path.is_file():
typer.echo(f"'{filename}' is not a file.", err=True)
raise typer.Exit(1)
raise click.UsageError(f"'{filename}' is not a file.")

content = file_path.read_text()
values = yaml.load(content)
Expand Down Expand Up @@ -297,7 +295,7 @@ def write_default_config(filename: Path) -> None:
"""
from ruamel.yaml.comments import CommentedMap

from ._attr_docs import attribute_docstrings
from generate_changelog._attr_docs import attribute_docstrings

file_path = filename.expanduser().resolve()
default_config = get_default_config()
Expand Down
8 changes: 4 additions & 4 deletions generate_changelog/data_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ def deep_merge(*dicts: dict) -> dict:
"""

def merge_into(d1: dict, d2: dict) -> dict:
for key in d2:
for key, val in d2.items():
if key not in d1 or not isinstance(d1[key], dict):
d1[key] = copy.deepcopy(d2[key])
d1[key] = copy.deepcopy(val)
else:
d1[key] = merge_into(d1[key], d2[key])
d1[key] = merge_into(d1[key], val)
return d1

return reduce(merge_into, dicts, {})
Expand Down Expand Up @@ -65,7 +65,7 @@ def comprehensive_merge(*args: Any) -> Any: # NOQA: C901
"""

def merge_into(d1: Any, d2: Any) -> Any:
if type(d1) != type(d2):
if type(d1) is not type(d2):
raise ValueError(f"Cannot merge {type(d2)} into {type(d1)}.")

if isinstance(d1, list):
Expand Down
4 changes: 2 additions & 2 deletions generate_changelog/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def get_section_pattern() -> str:
Get the version section pattern for the changelog.

Raises:
MissingConfiguration: If the ``starting_tag_pipeline`` configuration is missing or incorrect.
MissingConfigurationError: If the ``starting_tag_pipeline`` configuration is missing or incorrect.

Returns:
The version section pattern.
Expand Down Expand Up @@ -96,7 +96,7 @@ def get_changelog_path() -> Path:
Return the path to the changelog.

Raises:
MissingConfiguration: If the ``starting_tag_pipeline`` configuration is missing or incorrect.
MissingConfigurationError: If the ``starting_tag_pipeline`` configuration is missing or incorrect.

Returns:
The path to the changelog.
Expand Down
Loading
Loading