Skip to content

Commit 7475b70

Browse files
committed
libs: Add envoy.code.check[version_history]
Signed-off-by: Ryan Northey <ryan@synca.io>
1 parent d64301f commit 7475b70

File tree

7 files changed

+208
-1
lines changed

7 files changed

+208
-1
lines changed

envoy.code.check/envoy/code/check/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pytooling_library(
77
"//deps:aio.run.checker",
88
"//deps:envoy.base.utils",
99
"//deps:flake8",
10+
"//deps:packaging",
1011
"//deps:yapf",
1112
],
1213
sources=[
@@ -17,7 +18,9 @@ pytooling_library(
1718
"abstract/extensions.py",
1819
"abstract/flake8.py",
1920
"abstract/glint.py",
21+
"abstract/project.py",
2022
"abstract/shellcheck.py",
23+
"abstract/version_history.py",
2124
"abstract/yapf.py",
2225
"checker.py",
2326
"cmd.py",

envoy.code.check/envoy/code/check/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
AFileCodeCheck,
99
AGlintCheck,
1010
AShellcheckCheck,
11+
AVersionHistoryCheck,
1112
AYapfCheck)
1213
from .checker import (
1314
CodeChecker,
1415
ExtensionsCheck,
1516
Flake8Check,
1617
GlintCheck,
1718
ShellcheckCheck,
19+
VersionHistoryCheck,
1820
YapfCheck)
1921
from .cmd import run, main
2022
from . import checker
@@ -42,4 +44,5 @@
4244
"run",
4345
"ShellcheckCheck",
4446
"typing",
47+
"VersionHistoryCheck",
4548
"YapfCheck")

envoy.code.check/envoy/code/check/abstract/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .flake8 import AFlake8Check
66
from .glint import AGlintCheck
77
from .shellcheck import AShellcheckCheck
8+
from .version_history import AVersionHistoryCheck
89
from .yapf import AYapfCheck
910
from . import (
1011
base,
@@ -13,6 +14,7 @@
1314
flake8,
1415
glint,
1516
shellcheck,
17+
version_history,
1618
yapf)
1719

1820

@@ -24,11 +26,13 @@
2426
"AFlake8Check",
2527
"AGlintCheck",
2628
"AShellcheckCheck",
29+
"AVersionHistoryCheck",
2730
"AYapfCheck",
2831
"base",
2932
"checker",
3033
"flake8",
3134
"glint",
3235
"extensions",
3336
"shellcheck",
37+
"version_history",
3438
"yapf")

envoy.code.check/envoy/code/check/abstract/checker.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,16 @@ class ACodeChecker(
5151
"""Code checker."""
5252

5353
checks = (
54+
"changelogs_changes",
55+
"changelogs_pending",
5456
"extensions_fuzzed",
5557
"extensions_metadata",
5658
"extensions_registered",
5759
"glint",
5860
"python_yapf",
5961
"python_flake8",
60-
"shellcheck")
62+
"shellcheck",
63+
"version")
6164

6265
@property
6366
def all_files(self) -> bool:
@@ -192,6 +195,16 @@ def summary_class(self) -> Type[CodeCheckerSummary]:
192195
"""CodeChecker's summary class."""
193196
return CodeCheckerSummary
194197

198+
@cached_property
199+
def version_history(self) -> "abstract.AVersionHistoryCheck":
200+
"""VERSION_HISTORY checker."""
201+
return self.version_history_class(self.directory, fix=self.fix)
202+
203+
@property # type:ignore
204+
@abstracts.interfacemethod
205+
def version_history_class(self) -> Type["abstract.AVersionHistoryCheck"]:
206+
raise NotImplementedError
207+
195208
@cached_property
196209
def yapf(self) -> "abstract.AYapfCheck":
197210
"""YAPF checker."""
@@ -210,6 +223,32 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
210223
parser.add_argument("-s", "--since")
211224
parser.add_argument("--extensions_build_config")
212225

226+
async def check_changelogs_changes(self):
227+
for changelog in self.version_history.project.changelogs.values():
228+
errors = self.version_history.version_file(changelog).run_checks()
229+
if errors:
230+
self.error("changelogs", errors)
231+
else:
232+
self.succeed("changelogs", [f"{changelog.version}"])
233+
234+
async def check_changelogs_pending(self):
235+
pending = [
236+
k.base_version
237+
for k, v in self.version_history.project.changelogs.items()
238+
if v.release_date == "Pending"
239+
]
240+
all_good = (
241+
not pending
242+
or (self.version_history.project.is_dev and pending == [self.version_history.project.current_version.base_version]))
243+
if all_good:
244+
self.succeed("pending", [f"No extraneous pending versions found"])
245+
return
246+
pending = [x for x in pending if x != self.version_history.project.current_version.base_version]
247+
if self.version_history.project.is_dev:
248+
self.error("pending", [f"Only current version should be pending, found: {pending}"])
249+
else:
250+
self.error("pending", [f"Nothing should be pending, found: {pending}"])
251+
213252
async def check_extensions_fuzzed(self) -> None:
214253
"""Check for glint issues."""
215254
if self.extensions.all_fuzzed:
@@ -258,6 +297,33 @@ async def check_shellcheck(self) -> None:
258297
"""Check for shellcheck issues."""
259298
await self._code_check(self.shellcheck)
260299

300+
async def check_version(self) -> None:
301+
if self.version_history.project.is_current(self.version_history.project.current_changelog):
302+
self.succeed("version", ["VERSION.txt version matches most recent changelog"])
303+
else:
304+
self.error("version", ["VERSION.txt does not match most recent changelog"])
305+
not_pending = (
306+
self.version_history.project.is_dev and
307+
not self.version_history.project.changelogs[self.version_history.project.current_changelog].release_date == "Pending")
308+
not_dev = (
309+
not self.version_history.project.is_dev
310+
and self.version_history.project.changelogs[self.version_history.project.current_changelog].release_date == "Pending")
311+
if not_pending:
312+
self.error(
313+
"version",
314+
["VERSION.txt is set to `-dev` but most recent changelog is not `Pending`"])
315+
elif not_dev:
316+
self.error(
317+
"version",
318+
["VERSION.txt is not set to `-dev` but most recent changelog is `Pending`"])
319+
elif self.version_history.project.is_dev:
320+
self.succeed(
321+
"version", ["VERSION.txt is set to `-dev` and most recent changelog is `Pending`"])
322+
else:
323+
self.succeed(
324+
"version",
325+
["VERSION.txt is not set to `-dev` and most recent changelog is not `Pending`"])
326+
261327
@checker.preload(
262328
when=["python_flake8"],
263329
catches=[subprocess.exceptions.OSCommandError])
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import pathlib
2+
import re
3+
import sys
4+
from functools import cached_property
5+
from typing import Iterator, List, Pattern, Tuple
6+
7+
from packaging import version
8+
9+
import abstracts
10+
11+
from aio.run import checker
12+
13+
from envoy.code.check import abstract
14+
from .project import Project, VERSION_HISTORY_SECTIONS
15+
16+
INVALID_REFLINK = r"[^:]ref:`"
17+
REF_WITH_PUNCTUATION_REGEX = r".*\. <[^<]*>`\s*"
18+
19+
# Make sure backticks come in pairs.
20+
# Exceptions: reflinks (ref:`` where the backtick won't be preceded by a space
21+
# links `title <link>`_ where the _ is checked for in the regex.
22+
SINGLE_TICK_REGEX = re.compile(r"[^`]`[^`].*`[^`]")
23+
REF_TICKS_REGEX = re.compile(r":`[^`]*`")
24+
LINK_TICKS_REGEX = re.compile(r"[^`]`[^`].*`_")
25+
26+
27+
class VersionFile:
28+
29+
def __init__(self, changelog):
30+
self.changelog = changelog
31+
32+
@cached_property
33+
def invalid_reflink_re(self) -> Pattern[str]:
34+
return re.compile(INVALID_REFLINK)
35+
36+
@cached_property
37+
def link_ticks_re(self) -> Pattern[str]:
38+
return re.compile(LINK_TICKS_REGEX)
39+
40+
@cached_property
41+
def punctuation_re(self) -> Pattern[str]:
42+
return re.compile(REF_WITH_PUNCTUATION_REGEX)
43+
44+
@cached_property
45+
def ref_ticks_re(self) -> Pattern[str]:
46+
return re.compile(REF_TICKS_REGEX)
47+
48+
@cached_property
49+
def single_tick_re(self) -> Pattern[str]:
50+
return re.compile(SINGLE_TICK_REGEX)
51+
52+
def check_punctuation(self, section, entry) -> List[str]:
53+
change = entry["change"]
54+
if change.strip().endswith("."):
55+
return []
56+
# Ends with punctuated link
57+
if change.strip().endswith('`') and self.punctuation_re.match(change.strip()):
58+
return []
59+
# Ends with a list
60+
if change.strip().split("\n")[-1].startswith(" *"):
61+
return []
62+
return [
63+
f"{self.changelog.version}: Missing punctuation ({section}/{entry['area']}) ...{change[-30:]}\n{entry['change']}"
64+
]
65+
66+
def check_reflinks(self, section, entry) -> List[str]:
67+
return ([
68+
f"{self.changelog.version}: Found text \" ref:\" ({section}/{entry['area']}) This should probably be \" :ref:\"\n{entry['change']}"
69+
] if self.invalid_reflink_re.findall(entry["change"]) else [])
70+
71+
def check_change(self, section, entry):
72+
return [
73+
*self.check_reflinks(section, entry), *self.check_ticks(section, entry),
74+
*self.check_punctuation(section, entry)
75+
]
76+
77+
def check_ticks(self, section, entry) -> List[str]:
78+
_change = entry["change"]
79+
for reflink in self.ref_ticks_re.findall(_change):
80+
_change = _change.replace(reflink, "")
81+
for extlink in self.link_ticks_re.findall(_change):
82+
_change = _change.replace(extlink, "")
83+
single_ticks = self.single_tick_re.findall(_change)
84+
return ([
85+
f"{self.changelog.version}: Single backticks found ({section}/{entry['area']}) {', '.join(single_ticks)}\n{_change}"
86+
] if single_ticks else [])
87+
88+
def run_checks(self) -> Iterator[str]:
89+
errors = []
90+
for section, entries in self.changelog.data.items():
91+
if section == "date":
92+
continue
93+
if section not in VERSION_HISTORY_SECTIONS:
94+
errors.append(f"{self.changelog.version} Unrecognized changelog section: {section}")
95+
if section == "changes":
96+
if version.Version(self.changelog.version) > version.Version("1.16"):
97+
errors.append(f"Removed `changes` section found: {self.changelog.version}")
98+
if not entries:
99+
continue
100+
for entry in entries:
101+
errors.extend(self.check_change(section, entry))
102+
return errors
103+
104+
105+
class AVersionHistoryCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
106+
"""Extensions check."""
107+
108+
version_file = VersionFile
109+
_version_path = "VERSION.txt"
110+
111+
@property
112+
def changelogs(self) -> Tuple[pathlib.Path, ...]:
113+
return tuple(self.directory.path.joinpath("changelogs").glob("*.yaml"))
114+
115+
@cached_property
116+
def project(self):
117+
return Project(self.version_path, self.changelogs)
118+
119+
@cached_property
120+
def version_path(self):
121+
return self.directory.path.joinpath(self._version_path)

envoy.code.check/envoy/code/check/checker.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ class YapfCheck:
3434
pass
3535

3636

37+
@abstracts.implementer(check.AVersionHistoryCheck)
38+
class VersionHistoryCheck:
39+
pass
40+
41+
3742
@abstracts.implementer(check.ACodeChecker)
3843
class CodeChecker:
3944

@@ -65,6 +70,10 @@ def path(self) -> pathlib.Path:
6570
def shellcheck_class(self):
6671
return ShellcheckCheck
6772

73+
@property
74+
def version_history_class(self):
75+
return VersionHistoryCheck
76+
6877
@property
6978
def yapf_class(self):
7079
return YapfCheck

envoy.code.check/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ install_requires =
3333
aio.run.checker>=0.5.2
3434
envoy.base.utils>=0.1.0
3535
flake8
36+
packaging
3637
pep8-naming
3738
yapf
3839

0 commit comments

Comments
 (0)