"""Common file operations.""" import io import os import sys from gam import controlflow from gam import display from gam.var import GM_Globals from gam.var import GM_SYS_ENCODING from gam.var import UTF8_SIG def _open_file(filename, mode, encoding=None, newline=None): """Opens a file with no error handling.""" # Determine which encoding to use if 'b' in mode: encoding = None elif not encoding: encoding = GM_Globals[GM_SYS_ENCODING] elif 'r' in mode and encoding.lower().replace('-', '') == 'utf8': encoding = UTF8_SIG return open(os.path.expanduser(filename), mode, newline=newline, encoding=encoding) def open_file(filename, mode='r', encoding=None, newline=None, strip_utf_bom=False): """Opens a file. Args: filename: String, the name of the file to open, or '-' to use stdin/stdout, to read/write, depending on the mode param, respectively. mode: String, the common file mode to open the file with. Default is read. encoding: String, the name of the encoding used to decode or encode the file. This should only be used in text mode. newline: See param description in https://docs.python.org/3.7/library/functions.html#open strip_utf_bom: Boolean, True if the file being opened should seek past the UTF Byte Order Mark before being returned. See more: https://en.wikipedia.org/wiki/UTF-8#Byte_order_mark Returns: The opened file. """ try: if filename == '-': # Read from stdin, rather than a file if 'r' in mode: return io.StringIO(str(sys.stdin.read())) return sys.stdout # Open a file on disk f = _open_file(filename, mode, newline=newline, encoding=encoding) if strip_utf_bom: utf_bom = '\ufeff' has_bom = False if 'b' in mode: has_bom = f.read(3).decode('UTF-8') == utf_bom elif f.encoding and not f.encoding.lower().startswith('utf'): # Convert UTF BOM into ISO-8859-1 via Bytes utf8_bom_bytes = utf_bom.encode('UTF-8') iso_8859_1_bom = utf8_bom_bytes.decode('iso-8859-1').encode( 'iso-8859-1') has_bom = f.read(3).encode('iso-8859-1', 'replace') == iso_8859_1_bom else: has_bom = f.read(1) == utf_bom if not has_bom: f.seek(0) return f except OSError as e: controlflow.system_error_exit(6, e) def close_file(f, force_flush=False): """Closes a file. Args: f: The file to close force_flush: Flush file to disk emptying Python and OS caches. See: https://stackoverflow.com/a/13762137/1503886 Returns: Boolean, True if the file was successfully closed. False if an error was encountered while closing. """ if force_flush: f.flush() os.fsync(f.fileno()) try: f.close() return True except OSError as e: display.print_error(e) return False def read_file(filename, mode='r', encoding=None, newline=None, continue_on_error=False, display_errors=True): """Reads a file from disk. Args: filename: String, the path of the file to open from disk, or "-" to read from stdin. mode: String, the mode in which to open the file. encoding: String, the name of the encoding used to decode or encode the file. This should only be used in text mode. newline: See param description in https://docs.python.org/3.7/library/functions.html#open continue_on_error: Boolean, If True, suppresses any IO errors and returns to the caller without any externalities. display_errors: Boolean, If True, prints error messages when errors are encountered and continue_on_error is True. Returns: The contents of the file, or stdin if filename == "-". Returns None if an error is encountered and continue_on_errors is True. """ try: if filename == '-': # Read from stdin, rather than a file. return str(sys.stdin.read()) with _open_file(filename, mode, newline=newline, encoding=encoding) as f: return f.read() except OSError as e: if continue_on_error: if display_errors: display.print_warning(e) return None controlflow.system_error_exit(6, e) except (LookupError, UnicodeDecodeError, UnicodeError) as e: controlflow.system_error_exit(2, str(e)) def write_file(filename, data, mode='w', continue_on_error=False, display_errors=True): """Writes data to a file. Args: filename: String, the path of the file to write to disk. data: Serializable data to write to the file. mode: String, the mode in which to open the file and write to it. continue_on_error: Boolean, If True, suppresses any IO errors and returns to the caller without any externalities. display_errors: Boolean, If True, prints error messages when errors are encountered and continue_on_error is True. Returns: Boolean, True if the write operation succeeded, or False if not. """ try: with _open_file(filename, mode) as f: f.write(data) return True except OSError as e: if continue_on_error: if display_errors: display.print_error(e) return False else: controlflow.system_error_exit(6, e)