From 89f078740f7c391b2fcd5c25a817bdbed643bb4f Mon Sep 17 00:00:00 2001 From: Alexis Jeandet Date: Fri, 19 Jan 2024 16:49:40 +0100 Subject: [PATCH 1/5] Adds more projects description checks and refac to check everything per project Signed-off-by: Alexis Jeandet --- .github/workflows/PRs.yml | 2 +- .github/workflows/pr_checker.py | 107 +++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/.github/workflows/PRs.yml b/.github/workflows/PRs.yml index ebc6bf2..d37e6cd 100644 --- a/.github/workflows/PRs.yml +++ b/.github/workflows/PRs.yml @@ -14,7 +14,7 @@ jobs: python-version: "3.12" - name: Install dependencies run: | - python -m pip install pyyaml + python -m pip install pyyaml termcolor - name: Run PR checker run: | python .github/workflows/pr_checker.py \ No newline at end of file diff --git a/.github/workflows/pr_checker.py b/.github/workflows/pr_checker.py index fc2cf35..1ddce97 100644 --- a/.github/workflows/pr_checker.py +++ b/.github/workflows/pr_checker.py @@ -3,6 +3,9 @@ import os import itertools import argparse +from typing import Dict, Set +from functools import cache +from termcolor import colored, cprint here = os.path.dirname(__file__) root = os.path.abspath(f'{here}/../../') @@ -27,32 +30,102 @@ def ensure_all_yaml_files_are_valid(dry_run: bool = False): raise e +@cache def load_taxonomy(): with open(f"{root}/_data/taxonomy.yml", "r") as f: t = yaml.safe_load(f) return set(itertools.chain.from_iterable(map(lambda x: x["keywords"], t))) -def check_keywords_respect_taxonomy(projects_file: str, dry_run: bool = False): +def check_project_keywords_respect_taxonomy(project: Dict, dry_run: bool = False) -> bool: allowed_keywords: set = load_taxonomy() - with open(projects_file, "r") as f: - projects = yaml.safe_load(f) - for project in projects: - if "keywords" in project: - project_keywords = set(project["keywords"]) - unlisted_keywords = project_keywords.difference(allowed_keywords) - if len(unlisted_keywords) > 0: - if dry_run: - print(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") - else: - raise Exception(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") + if "keywords" in project: + project_keywords = set(project["keywords"]) + unlisted_keywords = project_keywords.difference(allowed_keywords) + if len(unlisted_keywords) > 0: + if dry_run: + print(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") + return False else: - print(f"Project {project['name']} has no keywords") + raise Exception(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") + else: + print(f"Project {project['name']} has no keywords") + return False + return True -if __name__ == "__main__": +@cache +def functionally_related_keywords() -> Set[str]: + with open(f"{root}/_data/taxonomy.yml", "r") as f: + t = yaml.safe_load(f) + return set(next(filter(lambda x: x["category"] == "Functionality", t))["keywords"]) + + +def check_project_has_mandatory_fields(project: Dict, dry_run: bool = False) -> bool: + mandatory_fields = { + "name", "description", "code", "contact", "keywords", "community", "documentation", "testing", + "software_maturity", + "python3", "license"} + + if len(mandatory_fields.difference(set(project.keys()))) > 0: + if dry_run: + print( + f"Project {project['name']} misses mandatory fields: {mandatory_fields.difference(set(project.keys()))}") + return False + else: + raise Exception( + f"Project {project['name']} misses mandatory fields: {mandatory_fields.difference(set(project.keys()))}") + + return True + + +def check_project_has_grades(project: Dict, dry_run: bool = False) -> bool: + grades = (["https://img.shields.io/badge/Requires%20improvement-red.svg", "Requires improvement"], + ["https://img.shields.io/badge/Partially%20met-orange.svg", "Partially met"], + ["https://img.shields.io/badge/Good-brightgreen.svg", "Good"]) + fields_with_grades = {"community", "documentation", "testing", "software_maturity", "python3", "license"} + for field in fields_with_grades: + if field in project and project[field] not in grades: + if dry_run: + print(f"Project {project['name']} deos not respect grades for field {field}, got {project[field]}") + return False + else: + raise Exception(f"Project {project['name']} deos not respect grades for field {field}") + return True + + +def check_project_has_functionality_related_keyword(project: Dict, dry_run: bool = False) -> bool: + functionality_related_keywords = functionally_related_keywords() + if len(set(project["keywords"]).intersection(functionality_related_keywords)) == 0: + if dry_run: + print(f"Project {project['name']} has no functionality related keyword") + return False + else: + raise Exception(f"Project {project['name']} has no functionality related keyword") + + return True + + +def main(): dry_run = parser.parse_args().dry_run ensure_all_yaml_files_are_valid(dry_run=dry_run) - check_keywords_respect_taxonomy(f"{root}/_data/projects_core.yml", dry_run=dry_run) - check_keywords_respect_taxonomy(f"{root}/_data/projects.yml", dry_run=dry_run) - check_keywords_respect_taxonomy(f"{root}/_data/projects_unevaluated.yml", dry_run=dry_run) + for projects_file in glob(f"{root}/_data/projects*.yml", recursive=True): + with open(projects_file, "r") as f: + projects = yaml.safe_load(f) + for project in projects: + print("-" * 80) + print(f"Checking project {project['name']} in file {projects_file}") + passes = all(( + check_project_has_mandatory_fields(project, dry_run=dry_run), + check_project_has_functionality_related_keyword(project, dry_run=dry_run), + check_project_keywords_respect_taxonomy(project, dry_run=dry_run), + check_project_has_grades(project, dry_run=dry_run) + )) + if not passes: + print(colored(f"Project {project['name']} failed checks", "red")) + else: + print(colored(f"Project {project['name']} passed checks", "green")) + + +if __name__ == "__main__": + main() From 58170f890dae957cb7b435a68a675db32bccb9ef Mon Sep 17 00:00:00 2001 From: Alexis Jeandet Date: Fri, 19 Jan 2024 17:23:50 +0100 Subject: [PATCH 2/5] PR checker do not raises anymore to always check all projects Instead it returns 1 if any project fails to pass the checks Signed-off-by: Alexis Jeandet --- .github/workflows/pr_checker.py | 49 ++++++++++++++------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/.github/workflows/pr_checker.py b/.github/workflows/pr_checker.py index 1ddce97..0013680 100644 --- a/.github/workflows/pr_checker.py +++ b/.github/workflows/pr_checker.py @@ -43,11 +43,8 @@ def check_project_keywords_respect_taxonomy(project: Dict, dry_run: bool = False project_keywords = set(project["keywords"]) unlisted_keywords = project_keywords.difference(allowed_keywords) if len(unlisted_keywords) > 0: - if dry_run: - print(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") - return False - else: - raise Exception(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") + print(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") + return False else: print(f"Project {project['name']} has no keywords") return False @@ -68,14 +65,9 @@ def check_project_has_mandatory_fields(project: Dict, dry_run: bool = False) -> "python3", "license"} if len(mandatory_fields.difference(set(project.keys()))) > 0: - if dry_run: - print( - f"Project {project['name']} misses mandatory fields: {mandatory_fields.difference(set(project.keys()))}") - return False - else: - raise Exception( - f"Project {project['name']} misses mandatory fields: {mandatory_fields.difference(set(project.keys()))}") - + print( + f"Project {project['name']} misses mandatory fields: {mandatory_fields.difference(set(project.keys()))}") + return False return True @@ -86,29 +78,23 @@ def check_project_has_grades(project: Dict, dry_run: bool = False) -> bool: fields_with_grades = {"community", "documentation", "testing", "software_maturity", "python3", "license"} for field in fields_with_grades: if field in project and project[field] not in grades: - if dry_run: - print(f"Project {project['name']} deos not respect grades for field {field}, got {project[field]}") - return False - else: - raise Exception(f"Project {project['name']} deos not respect grades for field {field}") + print(f"Project {project['name']} deos not respect grades for field {field}, got {project[field]}") + return False return True -def check_project_has_functionality_related_keyword(project: Dict, dry_run: bool = False) -> bool: +def check_project_has_functionality_related_keyword(project: Dict) -> bool: functionality_related_keywords = functionally_related_keywords() if len(set(project["keywords"]).intersection(functionality_related_keywords)) == 0: - if dry_run: - print(f"Project {project['name']} has no functionality related keyword") - return False - else: - raise Exception(f"Project {project['name']} has no functionality related keyword") - + print(f"Project {project['name']} has no functionality related keyword") + return False return True def main(): dry_run = parser.parse_args().dry_run ensure_all_yaml_files_are_valid(dry_run=dry_run) + any_failed = False for projects_file in glob(f"{root}/_data/projects*.yml", recursive=True): with open(projects_file, "r") as f: projects = yaml.safe_load(f) @@ -116,15 +102,20 @@ def main(): print("-" * 80) print(f"Checking project {project['name']} in file {projects_file}") passes = all(( - check_project_has_mandatory_fields(project, dry_run=dry_run), - check_project_has_functionality_related_keyword(project, dry_run=dry_run), - check_project_keywords_respect_taxonomy(project, dry_run=dry_run), - check_project_has_grades(project, dry_run=dry_run) + check_project_has_mandatory_fields(project), + check_project_has_functionality_related_keyword(project), + check_project_keywords_respect_taxonomy(project), + check_project_has_grades(project) )) if not passes: print(colored(f"Project {project['name']} failed checks", "red")) else: print(colored(f"Project {project['name']} passed checks", "green")) + any_failed = any_failed or not passes + if any_failed: + exit(1) + else: + exit(0) if __name__ == "__main__": From a721ea6f289119c65b771d543ae49df773c662d1 Mon Sep 17 00:00:00 2001 From: Alexis Jeandet Date: Fri, 19 Jan 2024 20:08:52 +0100 Subject: [PATCH 3/5] Ignore unevaluated projects Signed-off-by: Alexis Jeandet --- .github/workflows/pr_checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_checker.py b/.github/workflows/pr_checker.py index 0013680..00982d0 100644 --- a/.github/workflows/pr_checker.py +++ b/.github/workflows/pr_checker.py @@ -95,7 +95,7 @@ def main(): dry_run = parser.parse_args().dry_run ensure_all_yaml_files_are_valid(dry_run=dry_run) any_failed = False - for projects_file in glob(f"{root}/_data/projects*.yml", recursive=True): + for projects_file in (f"{root}/_data/projects.yml", f"{root}/_data/projects_core.yml"): with open(projects_file, "r") as f: projects = yaml.safe_load(f) for project in projects: From 18a6bbbcf96900785577846454a83b10f4387f03 Mon Sep 17 00:00:00 2001 From: Alexis Jeandet Date: Sun, 21 Jan 2024 16:25:44 +0100 Subject: [PATCH 4/5] Just warn if project uses no keyword Signed-off-by: Alexis Jeandet --- .github/workflows/pr_checker.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr_checker.py b/.github/workflows/pr_checker.py index 00982d0..a391558 100644 --- a/.github/workflows/pr_checker.py +++ b/.github/workflows/pr_checker.py @@ -42,6 +42,8 @@ def check_project_keywords_respect_taxonomy(project: Dict, dry_run: bool = False if "keywords" in project: project_keywords = set(project["keywords"]) unlisted_keywords = project_keywords.difference(allowed_keywords) + if len(project_keywords) == 0: + print(colored(f"Warning project {project['name']} has no keywords"), "yellow") if len(unlisted_keywords) > 0: print(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") return False @@ -83,14 +85,6 @@ def check_project_has_grades(project: Dict, dry_run: bool = False) -> bool: return True -def check_project_has_functionality_related_keyword(project: Dict) -> bool: - functionality_related_keywords = functionally_related_keywords() - if len(set(project["keywords"]).intersection(functionality_related_keywords)) == 0: - print(f"Project {project['name']} has no functionality related keyword") - return False - return True - - def main(): dry_run = parser.parse_args().dry_run ensure_all_yaml_files_are_valid(dry_run=dry_run) @@ -103,7 +97,6 @@ def main(): print(f"Checking project {project['name']} in file {projects_file}") passes = all(( check_project_has_mandatory_fields(project), - check_project_has_functionality_related_keyword(project), check_project_keywords_respect_taxonomy(project), check_project_has_grades(project) )) From c45477751b731cd635463f9fc82f218c657aa0d2 Mon Sep 17 00:00:00 2001 From: Alexis Jeandet Date: Mon, 22 Jan 2024 21:31:38 +0100 Subject: [PATCH 5/5] Fix GeospaceLAB and make keyword absence an error Signed-off-by: Alexis Jeandet --- .github/workflows/pr_checker.py | 3 ++- _data/projects.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_checker.py b/.github/workflows/pr_checker.py index a391558..70eb70e 100644 --- a/.github/workflows/pr_checker.py +++ b/.github/workflows/pr_checker.py @@ -43,7 +43,8 @@ def check_project_keywords_respect_taxonomy(project: Dict, dry_run: bool = False project_keywords = set(project["keywords"]) unlisted_keywords = project_keywords.difference(allowed_keywords) if len(project_keywords) == 0: - print(colored(f"Warning project {project['name']} has no keywords"), "yellow") + print(f"Project {project['name']} has no keywords") + return False if len(unlisted_keywords) > 0: print(f"Unlisted keywords for project {project['name']}: {unlisted_keywords}") return False diff --git a/_data/projects.yml b/_data/projects.yml index ed8f4ac..82e560c 100644 --- a/_data/projects.yml +++ b/_data/projects.yml @@ -60,7 +60,7 @@ contact: "Lei Cai" keywords: ["geospace","ionosphere_thermosphere_mesosphere","magnetosphere","data_retrieval","data_container","plotting","data_access", "data_analysis"] community: ["https://img.shields.io/badge/Good-brightgreen.svg", "Good"] - documentation: ["https://img.shields.io/badge/Good-brightgreen.svg", "Partially met"] + documentation: ["https://img.shields.io/badge/Good-brightgreen.svg", "Good"] testing: ["https://img.shields.io/badge/Good-brightgreen.svg", "Good"] software_maturity: ["https://img.shields.io/badge/Good-brightgreen.svg", "Good"] python3: ["https://img.shields.io/badge/Good-brightgreen.svg", "Good"]