diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index e8dc8bea..6135df83 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -12,35 +12,39 @@ # Returns ticket, category and counter or (None, None, None) if the basename -# could not be parsed +# could not be parsed or doesn't contain a valid category. def parse_newfragment_basename(basename, definitions): - parts = basename.split(u".") + invalid = (None, None, None) + parts = basename.split(".") if len(parts) == 1: - return (None, None, None) + return invalid if len(parts) == 2: ticket, category = parts - return ticket, category, 0 - - # fix-1.2.3.feature and fix.1.feature.2 are valid formats. The former is - # used in projects which don't put ticket numbers to newfragment names. - if parts[-1] in definitions: - category = parts[-1] - ticket = parts[-2] - return ticket, category, 0 - - # If there is a number after the category then use it as a counter, - # otherwise ignore it. - # This means 1.feature.1 and 1.feature do not conflict but - # 1.feature.rst and 1.feature do. - counter = 0 - try: - counter = int(parts[-1]) - except ValueError: - pass - category = parts[-2] - ticket = parts[-3] - return ticket, category, counter + return (ticket, category, 0) if category in definitions else invalid + + # There are at least 3 parts. Search for a valid category from the second + # part onwards. + # The category is used as the reference point in the parts list to later + # infer the issue number and counter value. + for i in range(1, len(parts)): + if parts[i] in definitions: + # Current part is a valid category according to given definitions. + category = parts[i] + # Use the previous part as the ticket number. + # NOTE: This allows news fragment names like fix-1.2.3.feature or + # something-cool.feature.ext for projects that don't use ticket + # numbers in news fragment names. + ticket = parts[i-1] + counter = 0 + # Use the following part as the counter if it exists and is a valid + # digit. + if len(parts) > (i + 1) and parts[i+1].isdigit(): + counter = int(parts[i+1]) + return ticket, category, counter + else: + # No valid category found. + return invalid # Returns a structure like: @@ -81,7 +85,7 @@ def find_fragments(base_directory, sections, fragment_directory, definitions): ticket, category, counter = parse_newfragment_basename( basename, definitions ) - if category is None or category not in definitions: + if category is None: continue full_filename = os.path.join(section_dir, basename) diff --git a/src/towncrier/newsfragments/173.feature.rst b/src/towncrier/newsfragments/173.feature.rst new file mode 100644 index 00000000..feaa1d7e --- /dev/null +++ b/src/towncrier/newsfragments/173.feature.rst @@ -0,0 +1,3 @@ + Improve news fragment file name parsing to allow using file names like + ``123.feature.1.ext`` which are convenient when one wants to use an appropriate + extension (e.g. ``rst``, ``md``) to enable syntax highlighting. diff --git a/src/towncrier/test/test_builder.py b/src/towncrier/test/test_builder.py index d5db5ab7..e6d0ed01 100644 --- a/src/towncrier/test/test_builder.py +++ b/src/towncrier/test/test_builder.py @@ -13,12 +13,24 @@ def test_simple(self): ("123", "feature", 0), ) + def test_invalid_category(self): + self.assertEqual( + parse_newfragment_basename("README.ext", ["feature"]), + (None, None, None), + ) + def test_counter(self): self.assertEqual( parse_newfragment_basename("123.feature.1", ["feature"]), ("123", "feature", 1), ) + def test_counter_with_extension(self): + self.assertEqual( + parse_newfragment_basename("123.feature.1.ext", ["feature"]), + ("123", "feature", 1), + ) + def test_ignores_extension(self): self.assertEqual( parse_newfragment_basename("123.feature.ext", ["feature"]), @@ -31,15 +43,22 @@ def test_non_numeric_ticket(self): ("baz", "feature", 0), ) + def test_non_numeric_ticket_with_extension(self): + self.assertEqual( + parse_newfragment_basename("baz.feature.ext", ["feature"]), + ("baz", "feature", 0), + ) + def test_dots_in_ticket_name(self): self.assertEqual( parse_newfragment_basename("baz.1.2.feature", ["feature"]), ("2", "feature", 0), ) - def test_dots_in_ticket_name_unknown_category(self): + def test_dots_in_ticket_name_invalid_category(self): self.assertEqual( - parse_newfragment_basename("baz.1.2.notfeature", ["feature"]), ("1", "2", 0) + parse_newfragment_basename("baz.1.2.notfeature", ["feature"]), + (None, None, None), ) def test_dots_in_ticket_name_and_counter(self):