Skip to content

Commit f08d922

Browse files
committed
Changed framework from Typer to Click
1 parent 06d7a45 commit f08d922

9 files changed

Lines changed: 88 additions & 106 deletions

File tree

generate_changelog/_attr_docs.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ def attribute_docstrings(obj: type) -> dict:
1616
nodes = list(ast_class.body)
1717
docstrings = {}
1818

19-
for (
20-
a,
21-
b,
22-
) in pairs(nodes):
19+
for a, b in pairs(nodes):
2320
if isinstance(a, ast.AnnAssign) and isinstance(a.target, ast.Name) and a.simple:
2421
name = a.target.id
2522
elif isinstance(a, ast.Assign) and len(a.targets) == 1 and isinstance(a.targets[0], ast.Name):

generate_changelog/actions/file_processing.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dataclasses import dataclass
55
from pathlib import Path
66

7-
import typer
7+
import rich_click as click
88

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

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

3635
return filepath.read_text() or ""
3736

@@ -55,7 +54,7 @@ def __call__(self, input_text: StrOrCallable) -> StrOrCallable:
5554
@register_builtin
5655
def stdout(content: str) -> str:
5756
"""Write content to stdout."""
58-
typer.echo(content)
57+
click.echo(content)
5958
return content
6059

6160

generate_changelog/actions/matching.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ class MetadataMatch:
7171
operator: in
7272
value: ["fix", "refactor", "update"]
7373
74-
Valid operators: ``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``, ``is``, ``is not``, ``in``, ``not in``
74+
Valid operators: `==`, `!=`, `<`, `>`, `>=`, `<=`, `is`, `is not`, `in`, `not in`
75+
76+
Attributes:
77+
operator_map: A map of operator names to operators
7578
7679
Args:
7780
attribute: The name of the metadata key whose value will be evaluated

generate_changelog/actions/shell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def bash(script: str, environment: Optional[dict] = None) -> str:
1818

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

21-
result = subprocess.run(
21+
result = subprocess.run( # NOQA: S603
2222
command,
2323
env=environment,
2424
encoding="utf-8",

generate_changelog/cli.py

Lines changed: 65 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,79 @@
22

33
import functools
44
import json
5-
from enum import Enum
65
from pathlib import Path
76
from typing import Callable, Optional
87

9-
import typer
8+
import rich_click as click
9+
from click.core import Context, Parameter
1010
from git import Repo
1111

12+
from generate_changelog import __version__
1213
from generate_changelog.commits import get_context_from_tags
1314
from generate_changelog.configuration import DEFAULT_CONFIG_FILE_NAMES, Configuration, write_default_config
1415
from generate_changelog.release_hint import suggest_release_type
1516

16-
app = typer.Typer()
1717

18-
19-
class OutputOption(str, Enum):
20-
"""Types of output available."""
21-
22-
release_hint = "release-hint"
23-
notes = "notes"
24-
all = "all"
25-
26-
27-
def version_callback(value: bool) -> None:
28-
"""Display the version and exit."""
29-
import generate_changelog
30-
31-
if value:
32-
typer.echo(generate_changelog.__version__)
33-
raise typer.Exit()
34-
35-
36-
def generate_config_callback(value: bool) -> None:
18+
def generate_config_callback(ctx: Context, param: Parameter, value: bool) -> None:
3719
"""Generate a default configuration file."""
3820
if not value: # pragma: no cover
3921
return
4022
f = Path.cwd() / Path(DEFAULT_CONFIG_FILE_NAMES[0])
4123
file_path = f.expanduser().resolve()
4224
if file_path.exists():
43-
overwrite = typer.confirm(f"{file_path} already exists. Overwrite it?")
25+
overwrite = click.confirm(f"{file_path} already exists. Overwrite it?")
4426
if not overwrite:
45-
typer.echo("Aborting configuration file generation.")
46-
typer.Abort()
27+
click.echo("Aborting configuration file generation.")
28+
click.Abort()
4729
write_default_config(f)
48-
typer.echo(f"The configuration file was written to {f}.")
49-
raise typer.Exit()
50-
51-
52-
@app.command()
53-
def main(
54-
version: Optional[bool] = typer.Option(
55-
None, "--version", help="Show program's version number and exit", callback=version_callback, is_eager=True
56-
),
57-
generate_config: Optional[bool] = typer.Option(
58-
None,
59-
"--generate-config",
60-
help="Generate a default configuration file",
61-
callback=generate_config_callback,
62-
),
63-
config_file: Optional[Path] = typer.Option(
64-
None, "--config", "-c", help="Path to the config file.", envvar="CHANGELOG_CONFIG_FILE"
65-
),
66-
repository_path: Optional[Path] = typer.Option(
67-
None, "--repo-path", "-r", help="Path to the repository, if not within the current directory"
68-
),
69-
starting_tag: Optional[str] = typer.Option(None, "--starting-tag", "-t", help="Tag to generate a changelog from."),
70-
output: Optional[OutputOption] = typer.Option(None, "--output", "-o", help="What output to generate."),
71-
skip_output_pipeline: bool = typer.Option(
72-
False, "--skip-output-pipeline", help="Do not execute the output pipeline in the configuration."
73-
),
74-
branch_override: Optional[str] = typer.Option(
75-
None, "--branch-override", "-b", help="Override the current branch for release hint decisions."
76-
),
30+
click.echo(f"The configuration file was written to {f}.")
31+
ctx.exit()
32+
33+
34+
@click.command(
35+
context_settings={
36+
"help_option_names": ["-h", "--help"],
37+
},
38+
add_help_option=True,
39+
)
40+
@click.option(
41+
"--generate-config",
42+
is_flag=True,
43+
help="Generate a default configuration file",
44+
callback=generate_config_callback,
45+
is_eager=True,
46+
expose_value=False,
47+
)
48+
@click.option(
49+
"--config",
50+
"-c",
51+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
52+
help="Path to the config file.",
53+
envvar="CHANGELOG_CONFIG_FILE",
54+
)
55+
@click.option("--repo-path", "-r", help="Path to the repository, if not within the current directory")
56+
@click.option("--starting-tag", "-t", help="Tag to generate a changelog from.")
57+
@click.option("--output", "-o", type=click.Choice(["release-hint", "notes", "all"]), help="What output to generate.")
58+
@click.option("--skip-output-pipeline", is_flag=True, help="Do not execute the output pipeline in the configuration.")
59+
@click.option("--branch-override", "-b", help="Override the current branch for release hint decisions.")
60+
@click.version_option(version=__version__)
61+
def cli(
62+
config: Optional[Path],
63+
repo_path: Optional[Path],
64+
starting_tag: Optional[str],
65+
output: Optional[str],
66+
skip_output_pipeline: bool,
67+
branch_override: Optional[str],
7768
) -> None:
7869
"""Generate a change log from git commits."""
7970
from generate_changelog import templating
8071
from generate_changelog.pipeline import pipeline_factory
8172

8273
echo_func = functools.partial(echo, quiet=bool(output))
83-
config = get_user_config(config_file, echo_func)
74+
configuration = get_user_config(config, echo_func)
8475

85-
if repository_path: # pragma: no cover
86-
repository = Repo(repository_path)
76+
if repo_path: # pragma: no cover
77+
repository = Repo(repo_path)
8778
else:
8879
repository = Repo(search_parent_directories=True)
8980

@@ -92,45 +83,45 @@ def main(
9283
else:
9384
current_branch = repository.active_branch
9485

95-
# get starting tag based configuration if not passed in
96-
if not starting_tag and config.starting_tag_pipeline:
97-
start_tag_pipeline = pipeline_factory(config.starting_tag_pipeline, **config.variables)
86+
# get starting tag based on configuration if not passed in
87+
if not starting_tag and configuration.starting_tag_pipeline:
88+
start_tag_pipeline = pipeline_factory(configuration.starting_tag_pipeline, **configuration.variables)
9889
starting_tag = start_tag_pipeline.run()
9990

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

105-
version_contexts = get_context_from_tags(repository, config, starting_tag)
96+
version_contexts = get_context_from_tags(repository, configuration, starting_tag)
10697

10798
branch_name = branch_override or current_branch.name
108-
release_hint = suggest_release_type(branch_name, version_contexts, config)
99+
release_hint = suggest_release_type(branch_name, version_contexts, configuration)
109100

110101
# use the output pipeline to deal with the rendered change log.
111102
has_starting_tag = bool(starting_tag)
112-
rendered_chglog = templating.render_changelog(version_contexts, config, has_starting_tag)
103+
rendered_chglog = templating.render_changelog(version_contexts, configuration, has_starting_tag)
113104

114105
if not skip_output_pipeline:
115106
echo_func("Executing output pipeline.")
116-
output_pipeline = pipeline_factory(config.output_pipeline, **config.variables)
107+
output_pipeline = pipeline_factory(configuration.output_pipeline, **configuration.variables)
117108
output_pipeline.run(rendered_chglog.full)
118109

119-
if output == OutputOption.release_hint:
120-
typer.echo(release_hint)
121-
elif output == OutputOption.notes:
110+
if output == "release_hint":
111+
click.echo(release_hint)
112+
elif output == "notes":
122113
if rendered_chglog.notes:
123-
typer.echo(rendered_chglog.notes)
114+
click.echo(rendered_chglog.notes)
124115
else:
125-
typer.echo(rendered_chglog.full)
126-
elif output == OutputOption.all:
116+
click.echo(rendered_chglog.full)
117+
elif output == "all":
127118
notes = rendered_chglog.notes or rendered_chglog.full
128119
out = {"release_hint": release_hint, "notes": notes}
129-
typer.echo(json.dumps(out))
120+
click.echo(json.dumps(out))
130121
else:
131-
typer.echo("Done.")
122+
click.echo("Done.")
132123

133-
raise typer.Exit()
124+
# raise click.Exit()
134125

135126

136127
def get_user_config(config_file: Optional[Path], echo_func: Callable) -> Configuration:
@@ -167,11 +158,4 @@ def echo(message: str, quiet: bool = False) -> None:
167158
quiet: Do it quietly
168159
"""
169160
if not quiet:
170-
typer.echo(message)
171-
172-
173-
typer_click_object = typer.main.get_command(app)
174-
175-
176-
if __name__ == "__main__":
177-
app()
161+
click.echo(message)

generate_changelog/configuration.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from dataclasses import asdict, dataclass, field
1616
from pathlib import Path
1717

18-
import typer
18+
import rich_click as click
1919
from ruamel.yaml import YAML
2020

2121
yaml = YAML()
@@ -244,17 +244,15 @@ def update_from_file(self, filename: Path) -> None:
244244
filename: Path to the YAML file
245245
246246
Raises:
247-
Exit: if the path does not exist or is a directory
247+
click.UsageError: if the path does not exist or is a directory
248248
"""
249249
file_path = filename.expanduser().resolve()
250250

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

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

259257
content = file_path.read_text()
260258
values = yaml.load(content)
@@ -297,7 +295,7 @@ def write_default_config(filename: Path) -> None:
297295
"""
298296
from ruamel.yaml.comments import CommentedMap
299297

300-
from ._attr_docs import attribute_docstrings
298+
from generate_changelog._attr_docs import attribute_docstrings
301299

302300
file_path = filename.expanduser().resolve()
303301
default_config = get_default_config()

generate_changelog/data_merge.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ def deep_merge(*dicts: dict) -> dict:
1919
"""
2020

2121
def merge_into(d1: dict, d2: dict) -> dict:
22-
for key in d2:
22+
for key, val in d2.items():
2323
if key not in d1 or not isinstance(d1[key], dict):
24-
d1[key] = copy.deepcopy(d2[key])
24+
d1[key] = copy.deepcopy(val)
2525
else:
26-
d1[key] = merge_into(d1[key], d2[key])
26+
d1[key] = merge_into(d1[key], val)
2727
return d1
2828

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

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

7171
if isinstance(d1, list):

generate_changelog/notes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def get_section_pattern() -> str:
6262
Get the version section pattern for the changelog.
6363
6464
Raises:
65-
MissingConfiguration: If the ``starting_tag_pipeline`` configuration is missing or incorrect.
65+
MissingConfigurationError: If the ``starting_tag_pipeline`` configuration is missing or incorrect.
6666
6767
Returns:
6868
The version section pattern.
@@ -96,7 +96,7 @@ def get_changelog_path() -> Path:
9696
Return the path to the changelog.
9797
9898
Raises:
99-
MissingConfiguration: If the ``starting_tag_pipeline`` configuration is missing or incorrect.
99+
MissingConfigurationError: If the ``starting_tag_pipeline`` configuration is missing or incorrect.
100100
101101
Returns:
102102
The path to the changelog.

generate_changelog/utilities.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ def resolve_name(obj: Any, name: str, default: Any = None) -> Any:
8383
The value at the resolved name or the default value.
8484
8585
Raises:
86-
TypeError, AttributeError: If accessing the property raises one of these exceptions.
86+
TypeError: If accessing the property raises one of these exceptions.
87+
AttributeError: If accessing the property raises one of these exceptions.
8788
"""
8889
lookups = name.split(".")
8990
current = obj
@@ -110,7 +111,7 @@ def resolve_name(obj: Any, name: str, default: Any = None) -> Any:
110111
): # un-subscript-able object
111112
return default
112113
return current
113-
except Exception: # NOQA: BLE001 pragma: no cover
114+
except Exception: # NOQA: BLE001 pragma: no cover
114115
return default
115116

116117

0 commit comments

Comments
 (0)