Repository: twisted/towncrier

Issue:
Add support for calling towncrier from sub-directories not only from the repo root.
I have a repo that looks like this

```
repo_root/
    - sub_project/
         - changes/ 
         - towncrier.toml
    - other_project/
```
where I want to use towncrier only in the sub_project directory (changes is where the fragments are). I've been able to create the first fragment in the default branch but when adding a new fragment `1234.feature` from a new branch and using the `check` command like

`towncrier check --compare-with origin/main ` 

this is what produces

```
Looking at these files:
----
1. /home/user/repo-root/sub_project/sub_project/changes/1234.feature
----
No new newsfragments found on this branch.

```

It has found the new fragment but doesn't recognize it as a new fragment. Also the directory that has checked 
is `sub_project/sub_project`, which it doesn't exist.



Discussion:
adiroiban: Thanks for the cleanup for `load_config_from_options`. Much appreciated.

Patch:
diff --git a/src/towncrier/_settings/load.py b/src/towncrier/_settings/load.py
index 66a6c54..724a776 100644
--- a/src/towncrier/_settings/load.py
+++ b/src/towncrier/_settings/load.py
@@ -65,26 +65,53 @@ class ConfigError(ClickException):
 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")
diff --git a/src/towncrier/newsfragments/601.feature.rst b/src/towncrier/newsfragments/601.feature.rst
new file mode 100644
index 0000000..ca209ca
--- /dev/null
+++ b/src/towncrier/newsfragments/601.feature.rst
@@ -0,0 +1,1 @@
+Running ``towncrier`` will now traverse back up directories looking for the configuration file.


Test patch:
diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py
index 241425e..17ffad4 100644
--- a/src/towncrier/test/test_build.py
+++ b/src/towncrier/test/test_build.py
@@ -121,11 +121,21 @@ class TestCli(TestCase):
         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()
diff --git a/src/towncrier/test/test_settings.py b/src/towncrier/test/test_settings.py
index 6d1f604..b08384a 100644
--- a/src/towncrier/test/test_settings.py
+++ b/src/towncrier/test/test_settings.py
@@ -207,10 +207,25 @@ class TomlSettingsTests(TestCase):
 
         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.