""" Class to represent the parsing of different file types into Python objects. """ import json import logging from abc import ABC, abstractmethod from pathlib import Path from typing import Any, Dict, Type import tomlkit from ruamel.yaml import YAML, YAMLError from ruamel.yaml.compat import StringIO from samcli.lib.config.exceptions import FileParseException LOG = logging.getLogger(__name__) COMMENT_KEY = "__comment__" class FileManager(ABC): """ Abstract class to be overridden by file managers for specific file extensions. """ @staticmethod @abstractmethod def read(filepath: Path) -> Any: """ Read a file at a given path. Parameters ---------- filepath: Path The Path object that points to the file to be read. Returns ------- Any A dictionary-like representation of the contents at the filepath location. """ raise NotImplementedError("Read method not implemented.") @staticmethod @abstractmethod def write(document: dict, filepath: Path): """ Write a dictionary or dictionary-like object to a given file. Parameters ---------- document: dict The object to write. filepath: Path The final location for the document to be written. """ raise NotImplementedError("Write method not implemented.") @staticmethod @abstractmethod def put_comment(document: Any, comment: str) -> Any: """ Put a comment in a document object. Parameters ---------- document: Any The object to write comment: str The comment to include in the document. Returns ------- Any The new document, with the comment added to it. """ raise NotImplementedError("Put comment method not implemented.") class TomlFileManager(FileManager): """ Static class to read and write toml files. """ file_format = "TOML" @staticmethod def read(filepath: Path) -> Any: """ Read a TOML file at the given path. Parameters ---------- filepath: Path The Path object that points to the file to be read. Returns ------- Any A dictionary-like tomlkit.TOMLDocument object, which represents the contents of the TOML file at the provided location. """ toml_doc = tomlkit.document() try: txt = filepath.read_text() toml_doc = tomlkit.loads(txt) except OSError as e: LOG.debug(f"OSError occurred while reading {TomlFileManager.file_format} file: {str(e)}") except tomlkit.exceptions.TOMLKitError as e: raise FileParseException(e) from e return toml_doc @staticmethod def write(document: dict, filepath: Path): """ Write the contents of a dictionary or tomlkit.TOMLDocument to a TOML file at the provided location. Parameters ---------- document: dict The object to write. filepath: Path The final location for the TOML file to be written. """ if not document: LOG.debug("Nothing for TomlFileManager to write.") return toml_document = TomlFileManager._to_toml(document) if toml_document.get(COMMENT_KEY, None): # Remove dunder comments that may be residue from other formats toml_document.add(tomlkit.comment(toml_document.get(COMMENT_KEY, ""))) toml_document.pop(COMMENT_KEY) filepath.write_text(tomlkit.dumps(toml_document)) @staticmethod def put_comment(document: dict, comment: str) -> Any: """ Put a comment in a document object. Parameters ---------- document: Any The tomlkit.TOMLDocument object to write comment: str The comment to include in the document. Returns ------- Any The new TOMLDocument, with the comment added to it. """ document = TomlFileManager._to_toml(document) document.add(tomlkit.comment(comment)) return document @staticmethod def _to_toml(document: dict) -> tomlkit.TOMLDocument: """Ensure that a dictionary-like object is a TOMLDocument.""" return tomlkit.parse(tomlkit.dumps(document)) class YamlFileManager(FileManager): """ Static class to read and write yaml files. """ yaml = YAML() file_format = "YAML" @staticmethod def read(filepath: Path) -> Any: """ Read a YAML file at the given path. Parameters ---------- filepath: Path The Path object that points to the file to be read. Returns ------- Any A dictionary-like yaml object, which represents the contents of the YAML file at the provided location. """ yaml_doc = {} try: yaml_doc = YamlFileManager.yaml.load(filepath.read_text()) except OSError as e: LOG.debug(f"OSError occurred while reading {YamlFileManager.file_format} file: {str(e)}") except YAMLError as e: raise FileParseException(e) from e return yaml_doc @staticmethod def write(document: dict, filepath: Path): """ Write the contents of a dictionary to a YAML file at the provided location. Parameters ---------- document: dict The object to write. filepath: Path The final location for the YAML file to be written. """ if not document: LOG.debug("No document given to YamlFileManager to write.") return yaml_doc = YamlFileManager._to_yaml(document) if yaml_doc.get(COMMENT_KEY, None): # Comment appears at the top of doc yaml_doc.yaml_set_start_comment(document[COMMENT_KEY]) yaml_doc.pop(COMMENT_KEY) YamlFileManager.yaml.dump(yaml_doc, filepath) @staticmethod def put_comment(document: Any, comment: str) -> Any: """ Put a comment in a document object. Parameters ---------- document: Any The yaml object to write comment: str The comment to include in the document. Returns ------- Any The new yaml document, with the comment added to it. """ document = YamlFileManager._to_yaml(document) document.yaml_set_start_comment(comment) return document @staticmethod def _to_yaml(document: dict) -> Any: """ Ensure a dictionary-like object is a YAML document. Parameters ---------- document: dict A dictionary-like object to parse. Returns ------- Any A dictionary-like YAML object, as derived from `yaml.load()`. """ with StringIO() as stream: YamlFileManager.yaml.dump(document, stream) return YamlFileManager.yaml.load(stream.getvalue()) class JsonFileManager(FileManager): """ Static class to read and write json files. """ file_format = "JSON" INDENT_SIZE = 2 @staticmethod def read(filepath: Path) -> Any: """ Read a JSON file at a given path. Parameters ---------- filepath: Path The Path object that points to the file to be read. Returns ------- Any A dictionary representation of the contents of the JSON document. """ json_file = {} try: json_file = json.loads(filepath.read_text()) except OSError as e: LOG.debug(f"OSError occurred while reading {JsonFileManager.file_format} file: {str(e)}") except json.JSONDecodeError as e: raise FileParseException(e) from e return json_file @staticmethod def write(document: dict, filepath: Path): """ Write a dictionary or dictionary-like object to a JSON file. Parameters ---------- document: dict The JSON object to write. filepath: Path The final location for the document to be written. """ if not document: LOG.debug("No document given to JsonFileManager to write.") return with filepath.open("w") as file: json.dump(document, file, indent=JsonFileManager.INDENT_SIZE) @staticmethod def put_comment(document: Any, comment: str) -> Any: """ Put a comment in a JSON object. Parameters ---------- document: Any The JSON object to write comment: str The comment to include in the document. Returns ------- Any The new JSON dictionary object, with the comment added to it. """ document.update({COMMENT_KEY: comment}) return document FILE_MANAGER_MAPPER: Dict[str, Type[FileManager]] = { # keys ordered by priority ".toml": TomlFileManager, ".yaml": YamlFileManager, ".yml": YamlFileManager, # ".json": JsonFileManager, # JSON support disabled }