Skip to content

Commit

Permalink
Improve news fragment file name parsing
Browse files Browse the repository at this point in the history
The new version uses category as the reference point to later infer the
issue number and counter value.
As a consequence, it will return (None, None, None) if the base name
doesn't contain a valid category.

The new parser allows 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.
  • Loading branch information
tjanez committed Jan 9, 2020
1 parent fff788d commit 65eb2c5
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 27 deletions.
54 changes: 29 additions & 25 deletions src/towncrier/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/towncrier/newsfragments/173.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 21 additions & 2 deletions src/towncrier/test/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand All @@ -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):
Expand Down

0 comments on commit 65eb2c5

Please sign in to comment.