Skip to content

Commit

Permalink
Interactive create (#482)
Browse files Browse the repository at this point in the history
* Interactive create when no filename is passed

* Configurable eof newline

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add changes

* More tests for file extensions

* mypy fix

* Test interactive create for orphan news fragments

* Interactive create when no filename is passed

* Configurable eof newline

* Add changes

* More tests for file extensions

* mypy fix

* Test interactive create for orphan news fragments

* Document two other new features in the newsfragment

* Test improvements

* Fix test to use default extension

* remove obscure --eof-newline/--no-eof-newline cli option. Have a config option is enough.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Glyph <glyph@twistedmatrix.com>
Co-authored-by: Adi Roiban <adi.roiban@chevah.com>
Co-authored-by: Adi Roiban <adiroiban@gmail.com>
  • Loading branch information
5 people authored Apr 28, 2024
1 parent f6083bb commit dd41869
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 57 deletions.
7 changes: 5 additions & 2 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Create a news fragment in the directory that ``towncrier`` is configured to look

$ towncrier create 123.bugfix.rst

If you don't provide a file name, ``towncrier`` will prompt you for one.

``towncrier create`` will enforce that the passed type (e.g. ``bugfix``) is valid.

If the fragments directory does not exist, it will be created.
Expand All @@ -91,9 +93,10 @@ If that is the entire fragment name, a random hash will be added for you::
A string to use for content.
Default: an instructive placeholder.

.. option:: --edit
.. option:: --edit / --no-edit

Create file and start `$EDITOR` to edit it right away.
Whether to start ``$EDITOR`` to edit the news fragment right away.
Default: ``$EDITOR`` will be started unless you also provided content.


``towncrier check``
Expand Down
10 changes: 10 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ Top level keys

``"+"`` by default.

``create_eof_newline``
Ensure the content of a news fragment file created with ``towncrier create`` ends with an empty line.

``true`` by default.

``create_add_extension``
Add the ``filename`` option extension to news fragment files created with ``towncrier create`` if an extension is not explicitly provided.

``true`` by default.

Extra top level keys for Python projects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions src/towncrier/_settings/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class Config:
wrap: bool = False
all_bullets: bool = True
orphan_prefix: str = "+"
create_eof_newline: bool = True
create_add_extension: bool = True


class ConfigError(ClickException):
Expand Down
79 changes: 55 additions & 24 deletions src/towncrier/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from ._settings import config_option_help, load_config_from_options


DEFAULT_CONTENT = "Add your info here"


@click.command(name="create")
@click.pass_context
@click.option(
Expand All @@ -32,30 +35,32 @@
)
@click.option(
"--edit/--no-edit",
default=False,
default=None,
help="Open an editor for writing the newsfragment content.",
) # TODO: default should be true
)
@click.option(
"-c",
"--content",
type=str,
default="Add your info here",
default=DEFAULT_CONTENT,
help="Sets the content of the new fragment.",
)
@click.argument("filename")
@click.argument("filename", default="")
def _main(
ctx: click.Context,
directory: str | None,
config: str | None,
filename: str,
edit: bool,
edit: bool | None,
content: str,
) -> None:
"""
Create a new news fragment.
Create a new news fragment called FILENAME or pass the full path for a file.
Towncrier has a few standard types of news fragments, signified by the file extension.
If FILENAME is not provided, you'll be prompted to create it.
Towncrier has a few standard types of news fragments, signified by the file
extension.
\b
These are:
Expand All @@ -76,14 +81,34 @@ def __main(
directory: str | None,
config_path: str | None,
filename: str,
edit: bool,
edit: bool | None,
content: str,
) -> None:
"""
The main entry point.
"""
base_directory, config = load_config_from_options(directory, config_path)

filename_ext = ""
if config.create_add_extension:
ext = os.path.splitext(config.filename)[1]
if ext.lower() in (".rst", ".md"):
filename_ext = ext

if not filename:
prompt = "Issue number"
# Add info about adding orphan if config is set.
if config.orphan_prefix:
prompt += f" (`{config.orphan_prefix}` if none)"
issue = click.prompt(prompt)
fragment_type = click.prompt(
"Fragment type",
type=click.Choice(list(config.types)),
)
filename = f"{issue}.{fragment_type}"
if edit is None and content == DEFAULT_CONTENT:
edit = True

file_dir, file_basename = os.path.split(filename)
if config.orphan_prefix and file_basename.startswith(f"{config.orphan_prefix}."):
# Append a random hex string to the orphan news fragment base name.
Expand All @@ -94,15 +119,18 @@ def __main(
f"{file_basename[len(config.orphan_prefix):]}"
),
)
if len(filename.split(".")) < 2 or (
filename.split(".")[-1] not in config.types
and filename.split(".")[-2] not in config.types
filename_parts = filename.split(".")
if len(filename_parts) < 2 or (
filename_parts[-1] not in config.types
and filename_parts[-2] not in config.types
):
raise click.BadParameter(
"Expected filename '{}' to be of format '{{name}}.{{type}}', "
"where '{{name}}' is an arbitrary slug and '{{type}}' is "
"one of: {}".format(filename, ", ".join(config.types))
)
if filename_parts[-1] in config.types and filename_ext:
filename += filename_ext

if config.directory:
fragments_directory = os.path.abspath(
Expand Down Expand Up @@ -135,31 +163,34 @@ def __main(
)

if edit:
edited_content = _get_news_content_from_user(content)
if edited_content is None:
click.echo("Abort creating news fragment.")
if content == DEFAULT_CONTENT:
content = ""
content = _get_news_content_from_user(content)
if not content:
click.echo("Aborted creating news fragment due to empty message.")
ctx.exit(1)
content = edited_content

with open(segment_file, "w") as f:
f.write(content)
if config.create_eof_newline and content and not content.endswith("\n"):
f.write("\n")

click.echo(f"Created news fragment at {segment_file}")


def _get_news_content_from_user(message: str) -> str | None:
initial_content = (
"# Please write your news content. When finished, save the file.\n"
"# In order to abort, exit without saving.\n"
'# Lines starting with "#" are ignored.\n'
)
initial_content += f"\n{message}\n"
def _get_news_content_from_user(message: str) -> str:
initial_content = """
# Please write your news content. Lines starting with '#' will be ignored, and
# an empty message aborts.
"""
if message:
initial_content = f"{message}\n{initial_content}"
content = click.edit(initial_content)
if content is None:
return None
return message
all_lines = content.split("\n")
lines = [line.rstrip() for line in all_lines if not line.lstrip().startswith("#")]
return "\n".join(lines)
return "\n".join(lines).strip()


if __name__ == "__main__": # pragma: no cover
Expand Down
5 changes: 5 additions & 0 deletions src/towncrier/newsfragments/482.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
If no filename is given when doing ``towncrier`` create, interactively ask for the issue number and fragment type (and then launch an interactive editor for the fragment content).

Now by default, when creating a fragment it will be appended with the ``filename`` option's extension (unless an extension is explicitly provided). For example, ``towncrier create 123.feature`` will create ``news/123.feature.rst``. This can be changed in configuration file by setting `add_extension = false`.

A new line is now added by default to the end of the fragment contents. This can be reverted in the configuration file by setting `add_newline = false`.
Loading

0 comments on commit dd41869

Please sign in to comment.