Skip to content

Commit

Permalink
Config location separate from directory containing news file and frag…
Browse files Browse the repository at this point in the history
…ments (#548)

* Build command supports multiple projects with one config

The create command already supports this.

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

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

* Full documented support for sharing config between multiple projects

* Apply suggestions from code review

Co-authored-by: Adi Roiban <[email protected]>

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Adi Roiban <[email protected]>
Co-authored-by: Adi Roiban <[email protected]>
  • Loading branch information
4 people authored Oct 22, 2023
1 parent 3f24b6e commit 0b023fa
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 8 deletions.
3 changes: 2 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ The following options can be passed to all of the commands that explained below:

.. option:: --dir PATH

Build fragment in ``PATH``.
The command is executed relative to ``PATH``.
For instance with the default config news fragments are checked and added in ``PATH/newsfragments`` and the news file is built in ``PATH/NEWS.rst``.

Default: current directory.

Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ Top level keys
The directory storing your news fragments.

For Python projects that provide a ``package`` key, the default is a ``newsfragments`` directory within the package.
Otherwise the default is a ``newsfragments`` directory relative to the configuration file.
Otherwise the default is a ``newsfragments`` directory relative to either the directory passed as ``--dir`` or (by default) the configuration file.

``filename``
The filename of your news file.

``"NEWS.rst"`` by default.
Its location is determined the same way as the location of the directory storing the news fragments.

``template``
Path to the template for generating the news file.
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Narrative

tutorial
markdown
monorepo


Reference
Expand Down
52 changes: 52 additions & 0 deletions docs/monorepo.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Multiple Projects Share One Config (Monorepo)
=============================================

Several projects may have independent release notes with the same format.
For instance packages in a monorepo.
Here's how you can use towncrier to set this up.

Below is a minimal example:

.. code-block:: text
repo
├── project_a
│ ├── newsfragments
│ │ └── 123.added
│ ├── project_a
│ │ └── __init__.py
│ └── NEWS.rst
├── project_b
│ ├── newsfragments
│ │ └── 120.bugfix
│ ├── project_b
│ │ └── __init__.py
│ └── NEWS.rst
└── towncrier.toml
The ``towncrier.toml`` looks like this:

.. code-block:: toml
[tool.towncrier]
# It's important to keep these config fields empty
# because we have more than one package/name to manage.
package = ""
name = ""
Now to add a fragment:

.. code-block:: console
towncrier create --config towncrier.toml --dir project_a 124.added
This should create a file at ``project_a/newsfragments/124.added``.

To build the news file for the same project:

.. code-block:: console
towncrier build --config towncrier.toml --dir project_a --version 1.5
Note that we must explicitly pass ``--version``, there is no other way to get the version number.
The ``towncrier.toml`` can only contain one version number and the ``package`` field is of no use for the same reason.
4 changes: 3 additions & 1 deletion src/towncrier/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ def __main(
click.echo("Finding news fragments...", err=to_err)

if config.directory is not None:
fragment_base_directory = os.path.abspath(config.directory)
fragment_base_directory = os.path.abspath(
os.path.join(base_directory, config.directory)
)
fragment_directory = None
else:
fragment_base_directory = os.path.abspath(
Expand Down
10 changes: 5 additions & 5 deletions src/towncrier/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ def __main(
)
sys.exit(0)

files = {
os.path.normpath(os.path.join(base_directory, path)) for path in files_changed
}
files = {os.path.abspath(path) for path in files_changed}

click.echo("Looking at these files:")
click.echo("----")
Expand All @@ -109,7 +107,9 @@ def __main(
sys.exit(0)

if config.directory:
fragment_base_directory = os.path.abspath(config.directory)
fragment_base_directory = os.path.abspath(
os.path.join(base_directory, config.directory)
)
fragment_directory = None
else:
fragment_base_directory = os.path.abspath(
Expand All @@ -118,7 +118,7 @@ def __main(
fragment_directory = "newsfragments"

fragments = {
os.path.normpath(path)
os.path.abspath(path)
for path in find_fragments(
fragment_base_directory,
config.sections,
Expand Down
2 changes: 2 additions & 0 deletions src/towncrier/newsfragments/548.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Initial support was added for monorepo-style setup.
One project with multiple independent news files stored in separate sub-directories, that share the same towncrier config.
27 changes: 27 additions & 0 deletions src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ def test_in_different_dir_config_option(self, runner):
self.assertEqual(0, result.exit_code)
self.assertTrue((project_dir / "NEWS.rst").exists())

@with_isolated_runner
def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner):
"""
Using the `--dir` CLI argument, the NEWS file can
be generated in a sub-directory from fragments
that are relatives to that sub-directory.
The path passed to `--dir` becomes the
working directory.
"""
Path("pyproject.toml").write_text(
"[tool.towncrier]\n" + 'directory = "changelog.d"\n'
)
Path("foo/foo").mkdir(parents=True)
Path("foo/foo/__init__.py").write_text("")
Path("foo/changelog.d").mkdir()
Path("foo/changelog.d/123.feature").write_text("Adds levitation")
self.assertFalse(Path("foo/NEWS.rst").exists())

result = runner.invoke(
cli,
("--yes", "--config", "pyproject.toml", "--dir", "foo", "--version", "1.0"),
)

self.assertEqual(0, result.exit_code)
self.assertTrue(Path("foo/NEWS.rst").exists())

@with_isolated_runner
def test_no_newsfragment_directory(self, runner):
"""
Expand Down
98 changes: 98 additions & 0 deletions src/towncrier/test/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,101 @@ def test_get_default_compare_branch_fallback(self):

self.assertEqual("origin/master", branch)
self.assertTrue(w[0].message.args[0].startswith('Using "origin/master'))

@with_isolated_runner
def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner):
"""
It can check the fragments located in a sub-directory
that is specified using the `--dir` CLI argument.
"""
main_branch = "main"
Path("pyproject.toml").write_text(
# Important to customize `config.directory` because the default
# already supports this scenario.
"[tool.towncrier]\n"
+ 'directory = "changelog.d"\n'
)
subproject1 = Path("foo")
(subproject1 / "foo").mkdir(parents=True)
(subproject1 / "foo/__init__.py").write_text("")
(subproject1 / "changelog.d").mkdir(parents=True)
(subproject1 / "changelog.d/123.feature").write_text("Adds levitation")
initial_commit(branch=main_branch)
call(["git", "checkout", "-b", "otherbranch"])

# We add a code change but forget to add a news fragment.
write(subproject1 / "foo/somefile.py", "import os")
commit("add a file")
result = runner.invoke(
towncrier_check,
(
"--config",
"pyproject.toml",
"--dir",
str(subproject1),
"--compare-with",
"main",
),
)

self.assertEqual(1, result.exit_code)
self.assertTrue(
result.output.endswith("No new newsfragments found on this branch.\n")
)

# We add the news fragment.
fragment_path = (subproject1 / "changelog.d/124.feature").absolute()
write(fragment_path, "Adds gravity back")
commit("add a newsfragment")
result = runner.invoke(
towncrier_check,
("--config", "pyproject.toml", "--dir", "foo", "--compare-with", "main"),
)

self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(
result.output.endswith("Found:\n1. " + str(fragment_path) + "\n"),
(result.output, str(fragment_path)),
)

# We add a change in a different subproject without a news fragment.
# Checking subproject1 should pass.
subproject2 = Path("bar")
(subproject2 / "bar").mkdir(parents=True)
(subproject2 / "changelog.d").mkdir(parents=True)
write(subproject2 / "bar/somefile.py", "import os")
commit("add a file")
result = runner.invoke(
towncrier_check,
(
"--config",
"pyproject.toml",
"--dir",
subproject1,
"--compare-with",
"main",
),
)

self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(
result.output.endswith("Found:\n1. " + str(fragment_path) + "\n"),
(result.output, str(fragment_path)),
)

# Checking subproject2 should result in an error.
result = runner.invoke(
towncrier_check,
(
"--config",
"pyproject.toml",
"--dir",
subproject2,
"--compare-with",
"main",
),
)
self.assertEqual(1, result.exit_code)
self.assertTrue(
result.output.endswith("No new newsfragments found on this branch.\n")
)
34 changes: 34 additions & 0 deletions src/towncrier/test/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,37 @@ def test_create_orphan_fragment_custom_prefix(self, runner: CliRunner):
self.assertEqual(len(change.stem), 11)
# Check the remainder are all hex characters.
self.assertTrue(all(c in string.hexdigits for c in change.stem[3:]))

@with_isolated_runner
def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner):
"""
When the `--dir` CLI argument is passed,
it will create a new file in directory that is
created by combining the `--dir` value
with the `directory` option from the configuration
file.
"""
Path("pyproject.toml").write_text(
# Important to customize `config.directory` because the default
# already supports this scenario.
"[tool.towncrier]\n"
+ 'directory = "changelog.d"\n'
)
Path("foo/foo").mkdir(parents=True)
Path("foo/foo/__init__.py").write_text("")

result = runner.invoke(
_main,
(
"--config",
"pyproject.toml",
"--dir",
"foo",
"--content",
"Adds levitation.",
"123.feature",
),
)

self.assertEqual(0, result.exit_code)
self.assertTrue(Path("foo/changelog.d/123.feature").exists())

0 comments on commit 0b023fa

Please sign in to comment.