diff --git a/bin/_file_formatter.py b/bin/_file_formatter.py index 142e9bb87c..fb1d296826 100644 --- a/bin/_file_formatter.py +++ b/bin/_file_formatter.py @@ -7,15 +7,15 @@ class FileFormatter(ABC): - check: bool - write: bool + args: argparse.Namespace scanned_file_found: int unformatted_file_count: int - def __init__(self, check: bool, write: bool) -> None: - self.check = check - self.write = write + arg_parser: argparse.ArgumentParser + + def __init__(self, args: argparse.Namespace) -> None: + self.args = args self.scanned_file_found = 0 self.unformatted_file_count = 0 @@ -26,9 +26,8 @@ def description() -> str: """Return the description of the formatter.""" return "JSON file formatter" - @staticmethod @abstractmethod - def format(input_str: str) -> str: + def format(self, input_str: str) -> str: """Format method to formatted file content.""" @staticmethod @@ -41,6 +40,11 @@ def decode_exception() -> Type[Exception]: def file_extension() -> str: """Return file extension of files to format.""" + @classmethod + def config_additional_args(cls) -> None: + """Optionally configure additional args to arg parser.""" + pass + def process_file(self, file_path: str) -> None: with open(file_path, "r", encoding="utf-8") as f: file_str = f.read() @@ -48,12 +52,14 @@ def process_file(self, file_path: str) -> None: formatted_file_str = self.format(file_str) except self.decode_exception() as error: raise ValueError(f"{file_path}: Cannot decode the file content") from error + except Exception as error: + raise ValueError(f"{file_path}: Fail to process") from error if file_str != formatted_file_str: - if self.write: + if self.args.write: with open(file_path, "w", encoding="utf-8") as f: f.write(formatted_file_str) print(f"reformatted {file_path}") - if self.check: + if self.args.check: print(f"would reformat {file_path}") self.unformatted_file_count += 1 self.scanned_file_found += 1 @@ -69,9 +75,9 @@ def process_directory(self, directory_path: str) -> None: def output_summary(self) -> None: print(f"{self.scanned_file_found} file(s) scanned.") - if self.write: + if self.args.write: print(f"{self.unformatted_file_count} file(s) reformatted.") - if self.check: + if self.args.check: print(f"{self.unformatted_file_count} file(s) need reformat.") if self.unformatted_file_count: sys.exit(-1) @@ -79,15 +85,15 @@ def output_summary(self) -> None: @classmethod def main(cls) -> None: - parser = argparse.ArgumentParser(description=cls.description()) - parser.add_argument( + cls.arg_parser = argparse.ArgumentParser(description=cls.description()) + cls.arg_parser.add_argument( "paths", metavar="file|dir", type=str, nargs="+", help="file to format or directory containing files to format", ) - group = parser.add_mutually_exclusive_group() + group = cls.arg_parser.add_mutually_exclusive_group() group.add_argument( "-c", "--check", @@ -102,8 +108,10 @@ def main(cls) -> None: help="Edit files in-place. (Beware!)", ) - args = parser.parse_args() - formatter = cls(args.check, args.write) + cls.config_additional_args() + + args = cls.arg_parser.parse_args() + formatter = cls(args) for path in args.paths: if not os.path.exists(path): diff --git a/bin/json-format.py b/bin/json-format.py index 53de4599be..6948f5226b 100755 --- a/bin/json-format.py +++ b/bin/json-format.py @@ -17,8 +17,7 @@ class JSONFormatter(FileFormatter): def description() -> str: return "JSON file formatter" - @staticmethod - def format(input_str: str) -> str: + def format(self, input_str: str) -> str: """Opinionated format JSON file.""" obj = json.loads(input_str) return json.dumps(obj, indent=2, sort_keys=True) + "\n" diff --git a/bin/yaml-format.py b/bin/yaml-format.py index 137e1ba972..9adce233a6 100755 --- a/bin/yaml-format.py +++ b/bin/yaml-format.py @@ -2,13 +2,14 @@ """JSON file formatter (without prettier).""" import os import sys +from textwrap import dedent my_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, my_path + "/..") import re from io import StringIO -from typing import Type +from typing import Any, Dict, Type # We use ruamel.yaml for parsing yaml files because it can preserve comments from ruamel.yaml import YAML @@ -27,10 +28,11 @@ class YAMLFormatter(FileFormatter): def description() -> str: return "YAML file formatter" - @staticmethod - def format(input_str: str) -> str: + def format(self, input_str: str) -> str: """Opinionated format YAML file.""" obj = yaml.load(input_str) + if self.args.add_test_metadata: + self._add_test_metadata(obj) out_stream = StringIO() yaml.dump( obj, @@ -39,6 +41,16 @@ def format(input_str: str) -> str: # ruamel.yaml tends to add 2 empty lines at the bottom of the dump return re.sub(r"\n+$", "\n", out_stream.getvalue()) + @staticmethod + def _add_test_metadata(obj: Dict[str, Any]) -> None: + metadata = obj.get("Metadata", {}) + if not metadata: + metadata = obj["Metadata"] = {} + sam_transform_test_value = metadata.get("SamTransformTest") + if sam_transform_test_value is not None and sam_transform_test_value is not True: + raise ValueError(f"Unexpected Metadata.SamTransformTest value {sam_transform_test_value}") + metadata["SamTransformTest"] = True + @staticmethod def decode_exception() -> Type[Exception]: return YAMLError @@ -47,6 +59,18 @@ def decode_exception() -> Type[Exception]: def file_extension() -> str: return ".yaml" + @classmethod + def config_additional_args(cls) -> None: + cls.arg_parser.add_argument( + "--add-test-metadata", + action="store_true", + help=dedent( + """\ + Add the testing metadata to yaml file if it doesn't exist: + "Metadata: SamTransformTest: true" """ + ), + ) + if __name__ == "__main__": YAMLFormatter.main()