Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 19 additions & 27 deletions py/envoy.base.utils/envoy/base/utils/abstract/project/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
CHANGELOG_CURRENT_PATH = "changelogs/current.yaml"
CHANGELOG_CURRENT_DIR_PATH = "changelogs/current"
CHANGELOG_ENTRY_GLOB = "*/*.rst"
CHANGELOG_SECTIONS_PATH = "changelogs/sections.yaml"
CHANGELOG_AREAS_PATH = "changelogs/areas.yaml"
CHANGELOG_CONFIG_PATH = "changelogs/changelogs.yaml"
ENTRY_SEPARATOR = "__"
CHANGELOG_SUMMARY_PATH = "changelogs/summary.md"
CHANGELOG_URL_TPL = (
Expand Down Expand Up @@ -317,33 +316,30 @@ def section_re(self) -> re.Pattern[str]:
return re.compile(r"\n[a-z_]*:")

@cached_property
def sections(self) -> typing.ChangelogSectionsDict:
def config(self) -> typing.ChangelogConfigDict:
try:
return utils.from_yaml(
self.sections_path,
typing.ChangelogSectionsDict)
self.config_path,
typing.ChangelogConfigDict)
except _yaml.reader.ReaderError as e:
raise exceptions.ChangelogError(
"Failed to parse changelog sections "
f"({self.sections_path}): {e}")
"Failed to parse changelog config "
f"({self.config_path}): {e}")
except utils.TypeCastingError as e:
logger.warning(
"Changelog section parsing error: "
f"({self.sections_path})\n{e}")
return cast(typing.ChangelogSectionsDict, e.value)
"Changelog config parsing error: "
f"({self.config_path})\n{e}")
return cast(typing.ChangelogConfigDict, e.value)

@cached_property
def sections(self) -> typing.ChangelogSectionsDict:
return self.config["sections"]

@cached_property
def areas(self) -> typing.ChangelogAreasDict:
if not self.areas_path.exists():
return cast(typing.ChangelogAreasDict, {})
try:
return utils.from_yaml(
self.areas_path,
typing.ChangelogAreasDict)
except (_yaml.reader.ReaderError, utils.TypeCastingError) as e:
raise exceptions.ChangelogError(
"Failed to parse changelog areas "
f"({self.areas_path}): {e}")
return cast(
typing.ChangelogAreasDict,
self.config.get("areas", {}))

def validate_sections(
self,
Expand All @@ -367,16 +363,12 @@ def validate_sections(
raise exceptions.ChangelogParseError(
f"Unknown changelog section(s){where}: "
f"{', '.join(unknown)}. "
f"Valid sections come from {CHANGELOG_SECTIONS_PATH}.")
f"Valid sections come from {CHANGELOG_CONFIG_PATH}.")
return data

@property
def sections_path(self) -> pathlib.Path:
return self.project.path.joinpath(CHANGELOG_SECTIONS_PATH)

@property
def areas_path(self) -> pathlib.Path:
return self.project.path.joinpath(CHANGELOG_AREAS_PATH)
def config_path(self) -> pathlib.Path:
return self.project.path.joinpath(CHANGELOG_CONFIG_PATH)

@property
def summary_path(self) -> pathlib.Path:
Expand Down
6 changes: 6 additions & 0 deletions py/envoy.base.utils/envoy/base/utils/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class ChangelogAreaDict(BaseChangelogSectionDict):

ChangelogAreasDict = dict[str, ChangelogAreaDict]


class ChangelogConfigDict(TypedDict, total=False):
sections: ChangelogSectionsDict
areas: ChangelogAreasDict


VersionConfigDict = dict[str, str]


Expand Down
109 changes: 47 additions & 62 deletions py/envoy.base.utils/tests/test_abstract_project_changelogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,13 @@ def test_abstract_changelogs_rel_current_dir_path(patches):
@pytest.mark.parametrize(
"raises",
[None, Exception, yaml.reader.ReaderError, exceptions.TypeCastingError])
def test_abstract_changelogs_sections(patches, raises):
def test_abstract_changelogs_config(patches, raises):
changelogs = DummyChangelogs("PROJECT")
patched = patches(
"cast",
"utils.from_yaml",
"logger",
("AChangelogs.sections_path",
("AChangelogs.config_path",
dict(new_callable=PropertyMock)),
prefix="envoy.base.utils.abstract.project.changelog")

Expand All @@ -350,110 +350,94 @@ def test_abstract_changelogs_sections(patches, raises):
m_yaml.side_effect = error
if raises == Exception:
with pytest.raises(Exception):
changelogs.sections
changelogs.config
elif raises == yaml.reader.ReaderError:
with pytest.raises(exceptions.ChangelogError) as e:
changelogs.sections
changelogs.config
else:
assert (
changelogs.sections
changelogs.config
== (m_yaml.return_value
if not raises
else m_cast.return_value))

if raises == exceptions.TypeCastingError:
assert (
m_cast.call_args
== [(typing.ChangelogSectionsDict, error.value), {}])
== [(typing.ChangelogConfigDict, error.value), {}])
assert (
m_logger.warning.call_args
== [("Changelog section parsing error: "
== [("Changelog config parsing error: "
f"({m_path.return_value})\n{error}", ), {}])
else:
assert not m_logger.called
assert not m_cast.called

assert (
m_yaml.call_args
== [(m_path.return_value, typing.ChangelogSectionsDict), {}])
== [(m_path.return_value, typing.ChangelogConfigDict), {}])
assert (
("sections" in changelogs.__dict__)
("config" in changelogs.__dict__)
== (not raises or raises == exceptions.TypeCastingError))
if raises != yaml.reader.ReaderError:
return
assert (
e.value.args[0]
== ("Failed to parse changelog sections "
== ("Failed to parse changelog config "
f"({m_path.return_value}): {str(error)}"))


@pytest.mark.parametrize("exists", [True, False])
@pytest.mark.parametrize(
"raises",
[None, Exception, yaml.reader.ReaderError, exceptions.TypeCastingError])
def test_abstract_changelogs_areas(patches, exists, raises):
def test_abstract_changelogs_sections(patches):
changelogs = DummyChangelogs("PROJECT")
patched = patches(
"utils.from_yaml",
("AChangelogs.areas_path",
("AChangelogs.config",
dict(new_callable=PropertyMock)),
prefix="envoy.base.utils.abstract.project.changelog")

with patched as (m_yaml, m_path):
m_path.return_value.exists.return_value = exists
if exists and raises:
error = raises("AN ERROR OCCURRED", 7, 23, "Y", "Z")
m_yaml.side_effect = error
if not exists:
assert changelogs.areas == {}
elif raises == Exception:
with pytest.raises(Exception):
changelogs.areas
elif raises in (yaml.reader.ReaderError, exceptions.TypeCastingError):
with pytest.raises(exceptions.ChangelogError) as e:
changelogs.areas
else:
assert changelogs.areas == m_yaml.return_value
with patched as (m_config, ):
assert (
changelogs.sections
== m_config.return_value.__getitem__.return_value)

assert (
("areas" in changelogs.__dict__)
== (not raises or not exists))
if not exists:
assert not m_yaml.called
return
assert (
m_yaml.call_args
== [(m_path.return_value, typing.ChangelogAreasDict), {}])
if raises not in (yaml.reader.ReaderError, exceptions.TypeCastingError):
return
assert (
e.value.args[0]
== ("Failed to parse changelog areas "
f"({m_path.return_value}): {str(error)}"))
m_config.return_value.__getitem__.call_args
== [("sections", ), {}])
assert "sections" in changelogs.__dict__


def test_abstract_changelogs_sections_path():
project = MagicMock()
changelogs = DummyChangelogs(project)
def test_abstract_changelogs_areas(patches):
changelogs = DummyChangelogs("PROJECT")
patched = patches(
"cast",
("AChangelogs.config",
dict(new_callable=PropertyMock)),
prefix="envoy.base.utils.abstract.project.changelog")

with patched as (m_cast, m_config):
assert (
changelogs.areas
== m_cast.return_value)

assert (
changelogs.sections_path
== project.path.joinpath.return_value)
m_config.return_value.get.call_args
== [("areas", {}), {}])
assert (
project.path.joinpath.call_args
== [(abstract.project.changelog.CHANGELOG_SECTIONS_PATH, ), {}])
assert "sections_path" not in changelogs.__dict__
m_cast.call_args
== [(typing.ChangelogAreasDict,
m_config.return_value.get.return_value), {}])
assert "areas" in changelogs.__dict__


def test_abstract_changelogs_areas_path():
def test_abstract_changelogs_config_path():
project = MagicMock()
changelogs = DummyChangelogs(project)
assert (
changelogs.areas_path
changelogs.config_path
== project.path.joinpath.return_value)
assert (
project.path.joinpath.call_args
== [(abstract.project.changelog.CHANGELOG_AREAS_PATH, ), {}])
assert "areas_path" not in changelogs.__dict__
== [(abstract.project.changelog.CHANGELOG_CONFIG_PATH, ), {}])
assert "config_path" not in changelogs.__dict__


def test_abstract_changelogs_validate_sections():
Expand All @@ -480,7 +464,7 @@ def test_abstract_changelogs_validate_sections_unknown(path):

message = e.value.args[0]
assert "unknown" in message
assert abstract.project.changelog.CHANGELOG_SECTIONS_PATH in message
assert abstract.project.changelog.CHANGELOG_CONFIG_PATH in message
if path is None:
assert "changelogs/current.yaml" not in message
assert "(None)" not in message
Expand Down Expand Up @@ -1794,9 +1778,10 @@ async def test_abstract_changelog_data_unknown_section(tmp_path):
"unknown:\n"
" - area: api\n"
" change: changed\n")
tmp_path.joinpath("changelogs/sections.yaml").write_text(
"known:\n"
" title: Known\n")
tmp_path.joinpath("changelogs/changelogs.yaml").write_text(
"sections:\n"
" known:\n"
" title: Known\n")

project = MagicMock()
project.path = tmp_path
Expand Down
14 changes: 7 additions & 7 deletions py/envoy.code.check/envoy/code/check/abstract/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
MAX_VERSION_FOR_CHANGES_SECTION = "1.16"
try:
from envoy.base.utils.abstract.project.changelog import (
CHANGELOG_AREAS_PATH,
CHANGELOG_CONFIG_PATH,
)
except ImportError:
CHANGELOG_AREAS_PATH = "changelogs/areas.yaml"
CHANGELOG_CONFIG_PATH = "changelogs/changelogs.yaml"
VALID_CHANGELOG_AREA_RE = re.compile(r"^[a-z0-9_\-/]+$")
VALID_CHANGELOG_AREA_PATTERN = r"[a-z0-9_\-/]+"
CHANGELOG_AREAS_FILE = pathlib.Path(CHANGELOG_AREAS_PATH)
CHANGELOG_CONFIG_FILE = pathlib.Path(CHANGELOG_CONFIG_PATH)


@abstracts.implementer(interface.IChangelogChangesChecker)
Expand Down Expand Up @@ -135,7 +135,7 @@ def check_entry_filename(
if self.areas and area not in self.areas:
return (
f"{path}: Invalid area '{area}'. "
f"Valid areas come from {CHANGELOG_AREAS_PATH}")
f"Valid areas come from {CHANGELOG_CONFIG_PATH}")
if not slug:
return f"{path}: Slug part of filename is empty"
return None
Expand All @@ -150,19 +150,19 @@ def check_areas_file(self) -> tuple[str, ...]:
title_areas.setdefault(title, []).append(area)
if not VALID_CHANGELOG_AREA_RE.match(area):
errors.append(
f"{CHANGELOG_AREAS_FILE}: "
f"{CHANGELOG_CONFIG_FILE}: "
f"Invalid area key '{area}' "
f"(must match {VALID_CHANGELOG_AREA_PATTERN})")
if not VALID_CHANGELOG_AREA_RE.match(title):
errors.append(
f"{CHANGELOG_AREAS_FILE}: "
f"{CHANGELOG_CONFIG_FILE}: "
f"Invalid title '{title}' for area '{area}' "
f"(must match {VALID_CHANGELOG_AREA_PATTERN})")
for title, areas in sorted(title_areas.items()):
if len(areas) < 2:
continue
errors.append(
f"{CHANGELOG_AREAS_FILE}: "
f"{CHANGELOG_CONFIG_FILE}: "
f"Duplicate title '{title}' used by areas: "
f"{', '.join(sorted(areas))}")
return tuple(errors)
Expand Down
12 changes: 6 additions & 6 deletions py/envoy.code.check/tests/test_abstract_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ def test_changeschecker_check_entry_filename_invalid_area():

assert result is not None
assert "Invalid area" in result
assert "areas.yaml" in result
assert "changelogs.yaml" in result


def test_changeschecker_check_entry_filename_without_areas():
Expand All @@ -899,7 +899,7 @@ def test_changeschecker_check_areas_file_duplicate_titles():
assert (
changelog.check_areas_file()
== (
"changelogs/areas.yaml: Duplicate title "
"changelogs/changelogs.yaml: Duplicate title "
"'dup' used by areas: bar, baz",
))

Expand All @@ -912,9 +912,9 @@ def test_changeschecker_check_areas_file_invalid_titles():
assert (
changelog.check_areas_file()
== (
"changelogs/areas.yaml: Invalid title 'BAD' "
"changelogs/changelogs.yaml: Invalid title 'BAD' "
"for area 'a' (must match [a-z0-9_\\-/]+)",
"changelogs/areas.yaml: Invalid title 'bad.dot' "
"changelogs/changelogs.yaml: Invalid title 'bad.dot' "
"for area 'b' (must match [a-z0-9_\\-/]+)",
))

Expand All @@ -927,9 +927,9 @@ def test_changeschecker_check_areas_file_invalid_keys():
assert (
changelog.check_areas_file()
== (
"changelogs/areas.yaml: Invalid area key 'Bad' "
"changelogs/changelogs.yaml: Invalid area key 'Bad' "
"(must match [a-z0-9_\\-/]+)",
"changelogs/areas.yaml: Invalid area key 'bad.dot' "
"changelogs/changelogs.yaml: Invalid area key 'bad.dot' "
"(must match [a-z0-9_\\-/]+)",
))

Expand Down