Skip to content

Commit

Permalink
Allow towncrier to traverse back up directories looking for the con…
Browse files Browse the repository at this point in the history
…figuration file (#601)
  • Loading branch information
SmileyChris committed May 22, 2024
1 parent 6d96010 commit 1c3b87b
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 13 deletions.
49 changes: 38 additions & 11 deletions src/towncrier/_settings/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,53 @@ def __init__(self, *args: str, **kwargs: str):
def load_config_from_options(
directory: str | None, config_path: str | None
) -> tuple[str, Config]:
"""
Load the configuration from a given directory or specific configuration file.
Unless an explicit configuration file is given, traverse back from the given
directory looking for a configuration file.
Returns a tuple of the base directory and the parsed Config instance.
"""
if config_path is None:
if directory is None:
directory = os.getcwd()
return traverse_for_config(directory)

config_path = os.path.abspath(config_path)

# When a directory is provided (in addition to the config file), use it as the base
# directory. Otherwise use the directory containing the config file.
if directory is not None:
base_directory = os.path.abspath(directory)
config = load_config(base_directory)
else:
config_path = os.path.abspath(config_path)
if directory is None:
base_directory = os.path.dirname(config_path)
else:
base_directory = os.path.abspath(directory)
config = load_config_from_file(os.path.dirname(config_path), config_path)
base_directory = os.path.dirname(config_path)

if config is None:
raise ConfigError(f"No configuration file found.\nLooked in: {base_directory}")
if not os.path.isfile(config_path):
raise ConfigError(f"Configuration file '{config_path}' not found.")
config = load_config_from_file(base_directory, config_path)

return base_directory, config


def traverse_for_config(path: str | None) -> tuple[str, Config]:
"""
Search for a configuration file in the current directory and all parent directories.
Returns the directory containing the configuration file and the parsed configuration.
"""
start_directory = directory = os.path.abspath(path or os.getcwd())
while True:
config = load_config(directory)
if config is not None:
return directory, config

parent = os.path.dirname(directory)
if parent == directory:
raise ConfigError(
f"No configuration file found.\nLooked back from: {start_directory}"
)
directory = parent


def load_config(directory: str) -> Config | None:
towncrier_toml = os.path.join(directory, "towncrier.toml")
pyproject_toml = os.path.join(directory, "pyproject.toml")
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/601.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Running ``towncrier`` will now traverse back up directories looking for the configuration file.
12 changes: 11 additions & 1 deletion src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,21 @@ def test_in_different_dir_dir_option(self, runner):
self.assertEqual(0, result.exit_code)
self.assertTrue((project_dir / "NEWS.rst").exists())

@with_project()
def test_traverse_up_to_find_config(self, runner):
"""
When the current directory doesn't contain the configuration file, Towncrier
will traverse up the directory tree until it finds it.
"""
os.chdir("foo")
result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"])
self.assertEqual(0, result.exit_code, result.output)

@with_project()
def test_in_different_dir_config_option(self, runner):
"""
The current working directory and the location of the configuration
don't matter as long as we pass corrct paths to the directory and the
don't matter as long as we pass correct paths to the directory and the
config file.
"""
project_dir = Path(".").resolve()
Expand Down
17 changes: 16 additions & 1 deletion src/towncrier/test/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,25 @@ def test_load_no_config(self, runner: CliRunner):

self.assertEqual(
result.output,
f"No configuration file found.\nLooked in: {os.path.abspath(temp)}\n",
f"No configuration file found.\nLooked back from: {os.path.abspath(temp)}\n",
)
self.assertEqual(result.exit_code, 1)

@with_isolated_runner
def test_load_explicit_missing_config(self, runner: CliRunner):
"""
Calling the CLI with an incorrect explicit configuration file will exit with
code 1 and an informative message is sent to standard output.
"""
config = "not-there.toml"
result = runner.invoke(cli, ("--config", config))

self.assertEqual(result.exit_code, 1)
self.assertEqual(
result.output,
f"Configuration file '{os.path.abspath(config)}' not found.\n",
)

def test_missing_template(self):
"""
Towncrier will raise an exception saying when it can't find a template.
Expand Down

0 comments on commit 1c3b87b

Please sign in to comment.