Repository: twisted/towncrier

Issue:
start_string vs start_line
We have here some inconsitency between the namings `start_string` vs `start_line`
https://github.com/twisted/towncrier/blob/5c431028a3b699c74b162014e907272cbea8ac81/src/towncrier/_settings.py#L150

If you specify `start_line` in the `pyproject.toml`, it will not be recognized correctly.
We have here some inconsistency in the code base itself, & also in the documentation.

Should be easy to solve:
1. Decide for which option to go: `start_string` or `start_line`
2. Unify the codebase
3. Adapt the docs accordingly



Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=h1) Report
> Merging [#301](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=desc) (07f88be) into [master](https://codecov.io/gh/twisted/towncrier/commit/ef1be451c44b71112cf6b6b2f1e90a27dd5ff489?el=desc) (ef1be45) will **decrease** coverage by `5.18%`.
> The diff coverage is `75.00%`.

[![Impacted file tree graph](https://codecov.io/gh/twisted/towncrier/pull/301/graphs/tree.svg?width=650&height=150&src=pr&token=9npndnog2E)](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #301      +/-   ##
==========================================
- Coverage   95.37%   90.19%   -5.19%     
==========================================
  Files          20       11       -9     
  Lines        1038      469     -569     
  Branches      104       94      -10     
==========================================
- Hits          990      423     -567     
+ Misses         27       25       -2     
  Partials       21       21              
```


| [Impacted Files](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/towncrier/\_settings.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci9fc2V0dGluZ3MucHk=) | `95.94% <ø> (ø)` | |
| [src/towncrier/\_writer.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci9fd3JpdGVyLnB5) | `88.00% <66.66%> (ø)` | |
| [src/towncrier/build.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci9idWlsZC5weQ==) | `90.00% <100.00%> (ø)` | |
| [src/towncrier/test/test\_builder.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfYnVpbGRlci5weQ==) | | |
| [src/towncrier/test/test\_project.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfcHJvamVjdC5weQ==) | | |
| [src/towncrier/test/test\_write.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3Rfd3JpdGUucHk=) | | |
| [src/towncrier/test/test\_format.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfZm9ybWF0LnB5) | | |
| [src/towncrier/test/test\_check.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfY2hlY2sucHk=) | | |
| [src/towncrier/\_\_main\_\_.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci9fX21haW5fXy5weQ==) | | |
| [src/towncrier/test/test\_create.py](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfY3JlYXRlLnB5) | | |
| ... and [1 more](https://codecov.io/gh/twisted/towncrier/pull/301/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=footer). Last update [ef1be45...07f88be](https://codecov.io/gh/twisted/towncrier/pull/301?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

altendky: At the moment I'm planning on overriding this coverage complaint on a line touched only to make a variable name more consistent.  Perhaps at some point I'll attack the missing coverage directly.
altendky: > If we are doing the rename, my suggestion is to rename it to "start_marker" as I think it's a better name.

The actual name read out of the configuration file is not being changed in this PR.  The docs are being corrected and the code is being changed to match.  I agree with the sentiment that the name isn't great but wasn't shooting for breaking changes here.  If you agree with this path I'll skip those comments and address the rest.
adiroiban: >  I agree with the sentiment that the name isn't great but wasn't shooting for breaking changes here. If you agree with this path I'll skip those comments and address the rest.

Yes. This can be merged with the current name (via the approved action) :) Thanks!


Patch:
diff --git a/README.rst b/README.rst
index 1c79c9d..0e80587 100644
--- a/README.rst
+++ b/README.rst
@@ -137,7 +137,7 @@ Towncrier has the following global options, which can be specified in the toml f
     version = "1.2.3"  # project version if maintained separately
     name = "arbitrary project name"
     template = "path/to/template.rst"
-    start_line = "start of generated content"
+    start_string = "Text used to detect where to add the generated content in the middle of a file. Generated content added after this text. Newline auto added."
     title_format = "{name} {version} ({project_date})"  # or false if template includes title
     issue_format = "format string for {issue} (issue is the first part of fragment name)"
     underlines: "=-~"
diff --git a/src/towncrier/_settings.py b/src/towncrier/_settings.py
index f84dcb9..e1bdb2f 100644
--- a/src/towncrier/_settings.py
+++ b/src/towncrier/_settings.py
@@ -149,7 +149,7 @@ def parse_toml(base_path, config):
         "sections": sections,
         "types": types,
         "template": template,
-        "start_line": config.get("start_string", _start_string),
+        "start_string": config.get("start_string", _start_string),
         "title_format": config.get("title_format", _title_format),
         "issue_format": config.get("issue_format"),
         "underlines": config.get("underlines", _underlines),
diff --git a/src/towncrier/_writer.py b/src/towncrier/_writer.py
index 1a2c31d..a09a72d 100644
--- a/src/towncrier/_writer.py
+++ b/src/towncrier/_writer.py
@@ -13,7 +13,7 @@ import os
 
 
 def append_to_newsfile(
-    directory, filename, start_line, top_line, content, single_file=True
+    directory, filename, start_string, top_line, content, single_file=True
 ):
 
     news_file = os.path.join(directory, filename)
@@ -24,7 +24,7 @@ def append_to_newsfile(
         else:
             with io.open(news_file, "r", encoding="utf8") as f:
                 existing_content = f.read()
-        existing_content = existing_content.split(start_line, 1)
+        existing_content = existing_content.split(start_string, 1)
     else:
         existing_content = [u""]
 
@@ -35,11 +35,12 @@ def append_to_newsfile(
 
         if len(existing_content) > 1:
             f.write(existing_content.pop(0).rstrip().encode("utf8"))
-            if start_line:
-                f.write((u"\n\n" + start_line + u"\n").encode("utf8"))
+            if start_string:
+                f.write((u"\n\n" + start_string + u"\n").encode("utf8"))
 
         f.write(top_line.encode("utf8"))
         f.write(content.encode("utf8"))
-        if existing_content[0]:
-            f.write(b"\n\n")
-        f.write(existing_content[0].lstrip().encode("utf8"))
+        if existing_content:
+            if existing_content[0]:
+                f.write(b"\n\n")
+            f.write(existing_content[0].lstrip().encode("utf8"))
diff --git a/src/towncrier/build.py b/src/towncrier/build.py
index 18b606c..2f02eec 100644
--- a/src/towncrier/build.py
+++ b/src/towncrier/build.py
@@ -167,7 +167,7 @@ def __main(
         click.echo(rendered)
     else:
         click.echo("Writing to newsfile...", err=to_err)
-        start_line = config["start_line"]
+        start_string = config["start_string"]
         news_file = config["filename"]
 
         if config["single_file"]:
@@ -179,7 +179,7 @@ def __main(
         append_to_newsfile(
             base_directory,
             news_file,
-            start_line,
+            start_string,
             top_line,
             rendered,
             single_file=config["single_file"],
diff --git a/src/towncrier/newsfragments/277.doc.rst b/src/towncrier/newsfragments/277.doc.rst
new file mode 100644
index 0000000..ef22943
--- /dev/null
+++ b/src/towncrier/newsfragments/277.doc.rst
@@ -0,0 +1,1 @@
+``start_line`` corrected to ``start_string`` in the readme to match the long standing implementation.


Test patch:
diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py
index 29ad613..fb3955f 100644
--- a/src/towncrier/test/test_build.py
+++ b/src/towncrier/test/test_build.py
@@ -779,3 +779,60 @@ class TestCli(TestCase):
 
         self.assertEqual(0, result.exit_code)
         self.assertEqual(expected_output, result.output)
+
+    def test_start_string(self):
+        """
+        The `start_string` configuration is used to detect the starting point
+        for inserting the generated release notes. A newline is automatically
+        added to the configured value.
+        """
+        runner = CliRunner()
+
+        with runner.isolated_filesystem():
+            with open("pyproject.toml", "w") as f:
+                f.write(dedent("""\
+                    [tool.towncrier]
+                    start_string="Release notes start marker"
+                """))
+            os.mkdir("newsfragments")
+            with open("newsfragments/123.feature", "w") as f:
+                f.write("Adds levitation")
+            with open("NEWS.rst", "w") as f:
+                f.write("a line\n\nanother\n\nRelease notes start marker\n")
+
+            result = runner.invoke(
+                _main,
+                [
+                    "--version",
+                    "7.8.9",
+                    "--name",
+                    "foo",
+                    "--date",
+                    "01-01-2001",
+                    "--yes",
+                ],
+            )
+
+            self.assertEqual(0, result.exit_code, result.output)
+            self.assertTrue(os.path.exists("NEWS.rst"), os.listdir("."))
+            with open("NEWS.rst", "r") as f:
+                output = f.read()
+
+        expected_output = dedent("""\
+            a line
+
+            another
+
+            Release notes start marker
+            foo 7.8.9 (01-01-2001)
+            ======================
+
+            Features
+            --------
+
+            - Adds levitation (#123)
+
+
+        """)
+
+        self.assertEqual(expected_output, output)
diff --git a/src/towncrier/test/test_write.py b/src/towncrier/test/test_write.py
index 7ad4853..12c5bad 100644
--- a/src/towncrier/test/test_write.py
+++ b/src/towncrier/test/test_write.py
@@ -5,6 +5,7 @@ from twisted.trial.unittest import TestCase
 
 import pkg_resources
 import os
+from textwrap import dedent
 
 from collections import OrderedDict
 
@@ -226,3 +227,47 @@ Old text.
             output = f.read()
 
         self.assertEqual(expected_output, output)
+
+    def test_multiple_file_no_start_string(self):
+        """
+        When no `start_string` is defined, the generated content is added at
+        the start of the file.
+        """
+        tempdir = self.mktemp()
+        os.mkdir(tempdir)
+
+        definitions = {}
+        fragments = split_fragments(fragments={}, definitions=definitions)
+
+        template = pkg_resources.resource_string(
+            "towncrier", "templates/default.rst"
+        ).decode("utf8")
+
+        content = render_fragments(
+            template=template,
+            issue_format=None,
+            top_line="",
+            fragments=fragments,
+            definitions=definitions,
+            underlines=["-", "~"],
+            wrap=True,
+            versiondata={"name": "MyProject", "version": "1.0", "date": "never"},
+        )
+
+        append_to_newsfile(
+            directory=tempdir,
+            filename="NEWS.rst",
+            start_string=None,
+            top_line="",
+            content=content,
+        )
+
+        with open(os.path.join(tempdir, "NEWS.rst"), "r") as f:
+            output = f.read()
+
+        expected_output = dedent("""\
+            MyProject 1.0 (never)
+            =====================
+        """)
+
+        self.assertEqual(expected_output, output)