"""
This module provides some generic file and directory level operation that
include the following:
1. Check if path is a File or Directory via is_file(), is_dir()
2. Create a directory via create_dir()
3. Walk a directory tree based on single extension using walk_tree()
4. Resolve path including shell and user expansion along with getting realpath to file using resolve_path()
5. Read and write a file via read_file(), write_file()
"""
import json
import os
from buildtest.exceptions import BuildTestError
[docs]def is_file(fname):
"""Check if file exist and returns True/False
Args:
fname (str): file path to check
Returns:
bool: True if path is a file and is a realpath otherwise returns False
"""
# resolve_path will return the full canonical filename or return None if file doesn't exist
fname = resolve_path(fname)
# if return is None we return False since file is non-existent
if not fname:
return False
# at this stage we know it's a valid file but we don't know if its a file or directory
return os.path.isfile(fname)
[docs]def is_dir(dirname):
"""Check if input directory exist and is a directory. If so return ``True`` otherwise returns ``False``.
We resolve path by invoking :func:`resolve_path`
Args:
dirname (str): directory path to check
Returns:
bool: True if directory exists otherwise returns False.
"""
# resolve_path will return the full canonical directory name or return None if directory doesn't exist
dirname = resolve_path(dirname)
# if return is None we stop here and return False since directory is non-existent.
if not dirname:
return False
# at this stage we know it's a valid file, so return if file is a directory or not
return os.path.isdir(dirname)
[docs]def walk_tree(root_dir, ext=None):
"""This method will traverse a directory tree and return list of files
based on extension type. This method invokes :func:`is_dir` to check if directory
exists before traversal.
Args:
root_dir (str): directory path to traverse
ext (str): File extension to search in traversal
Returns:
list: A list of file paths for a directory traversal based on extension type. If ``ext`` is **None** we retrieve all files
"""
list_files = []
# if directory doesn't exist let's return empty list before doing a directory traversal since no files to traverse
if not is_dir(root_dir):
return list_files
for root, subdir, files in os.walk(root_dir):
for fname in files:
# if ext is provided check if file ends with extension and add to list, otherwise
# add all files to list and return
if ext:
if fname.endswith(ext):
list_files.append(os.path.join(root, fname))
else:
list_files.append(os.path.join(root, fname))
return list_files
[docs]def create_dir(dirname):
"""Create a directory if it doesn't exist. If directory contains variable
expansion (**$HOME**) or user expansion (**~**), we resolve this before creating directory.
If there is an error creating directory we raise an exception BuildTestError
:param dirname: directory path to create
:type dirname: str, required
:return: creates the directory or print an exception message upon failure
:rtype: Catches exception of type OSError and raise exception BuildTestError or returns None
"""
# these three lines implement same as ``resolve_path`` will return None when it's not a known file. We expect
# input to create_dir will be a non-existent path so we run these lines manually
dirname = os.path.expanduser(dirname)
dirname = os.path.expandvars(dirname)
dirname = os.path.realpath(dirname)
if not os.path.isdir(dirname):
try:
os.makedirs(dirname)
except OSError as err:
print(err)
raise BuildTestError(f"Cannot create directory {dirname}")
[docs]def resolve_path(path, exist=True):
"""This method will resolve a file path to account for shell expansion and resolve paths in
when a symlink is provided in the file. This method assumes file already exists.
Args:
path (str): file path to resolve
exist (bool): a boolean to determine if filepath should be returned if filepath doesn't exist on filesystem.
Returns:
str: Full path to file if file exists or ``exist=True`` is set. We could return ``None`` if path is not defined or file path doesn't exist and ``exist=False``
Raises:
BuildTestError: If input path is not of type str
>>> a = resolve_path("$HOME/.bashrc")
>>> assert a
>>> b = resolve_path("$HOME/.bashrc1", exist=False)
>>> assert b
>>> c = resolve_path("$HOME/.bashrc1", exist=True)
>>> assert not c
"""
# if path not set return None
if not path:
return
if not isinstance(path, str):
raise BuildTestError(
f"Input must be a string type, {path} is of type {type(path)}"
)
# apply shell expansion when file includes something like $HOME/example
path = os.path.expandvars(path)
# apply user expansion when file includes something like ~/example
path = os.path.expanduser(path)
real_path = os.path.realpath(path)
if os.path.exists(real_path) or not exist:
return real_path
[docs]def read_file(filepath):
"""This method is used to read a file and return content of file.
If filepath is not a string we raise an error. We run :func:`resolve_path`
to get realpath to file and account for shell or user expansion. The
return will be a valid file or ``None`` so we check if input is an invalid file.
Finally we read the file and return the content of the file as a string.
Args:
filepath (str): File name to read
Raises:
BuildTestError:
- if filepath is invalid
- filepath is not an instance of type :class:`str`.
- An exception can be raised if there is an issue reading file with an exception of :class:`IOError`
Returns:
str: content of input file
"""
# ensure filepath is a string, if not, we raise an error.
if not isinstance(filepath, str):
raise BuildTestError(
f"Invalid type for file: {filepath} must be of type 'str' "
)
input_file = filepath
# resolve_path will handle shell and user expansion and account for any symlinks and check for file existence.
# if resolve_path does not return gracefully it implies file does not exist and will return None
filepath = resolve_path(filepath)
# if it's invalid file let's raise an error
if not filepath:
raise BuildTestError(
f"Unable to find input file: {input_file}. Please specify a valid file"
)
try:
with open(filepath, "r") as fd:
content = fd.read()
except IOError as err:
raise BuildTestError("Failed to read: %s: %s" % (filepath, err))
return content
[docs]def write_file(filepath, content):
"""This method is used to write an input ``content`` to a file specified by
``filepath``. Both filepath and content must be a str. An error is raised
if filepath is not a string or a directory. If ``content`` is not a str,
we return ``None`` since we can't write the content to file.
Finally, we write the content to file and return. A successful write
will return nothing otherwise an exception will occur during the write
process.
Args:
filepath (str): file name to write
content (str): content to write to file
Raises:
BuildTestError:
- filepath is not :class:`str`
- filepath is directory via :class:`is_dir`
- content of file is not of type :class:`str`
- Error writing file with an exception of type :class:`IOError`
"""
# ensure filepath is a string, if not we raise an error
if not isinstance(filepath, str):
raise BuildTestError(
f"Invalid type for file: {filepath} must be of type 'str' "
)
# if filepath is a directory, we raise an exception noting that user must specify a filepath
if is_dir(filepath):
raise BuildTestError(
f"Detected {filepath} is a directory, please specify a file path."
)
# ensure content is of type string
if not isinstance(content, str):
raise BuildTestError(
f"Expecting type str but got type: {type(content)} when writing file"
)
try:
with open(filepath, "w") as fd:
fd.write(content)
except IOError as err:
raise BuildTestError(f"Failed to write: {filepath}: {err}")
[docs]def remove_file(fpath):
"""This method is responsible for removing a file. The input path is an absolute path
to file. We check for exceptions first, and return immediately before removing file.
Args:
fpath (str): full path to file to remove
Raises:
BuildTestError:
- If fpath is not instance of :class:`str`
- If fpath is not a file using :func:`is_file`
- An exception of type :class:`OSError` when removing file via :func:`os.remove`
"""
if not fpath:
return
if not isinstance(fpath, str):
raise BuildTestError(
f"Unable to remove file: {fpath} because we have a type mismatch. It must be a string type"
)
# if its not a file return
if not is_file(fpath):
raise BuildTestError(
f"The filepath: {fpath} must be a file and must exist on file system"
)
try:
os.remove(fpath)
except OSError:
raise BuildTestError(f"Unable to delete file: {fpath}")
[docs]def load_json(fname):
"""Given a filename, resolves full path to file and loads json file. This method will
catch exception :class:`json.JSONDecodeError` and raise an exception with useful message. If there is no
error we return content of json file
Args:
fname (str): Name of file to read and load json content
Raises:
BuildTestError: Raise exception if file is not resolved via :func:`resolve_path` or failure to load JSON document
"""
abspath_fname = resolve_path(fname)
# if filename doesn't exist we raise an exception
if not abspath_fname:
raise BuildTestError(f"Unable to resolve path: {fname}")
# attempt to open file for reading and use json.loads to read the content and check for exception
with open(abspath_fname) as fd:
try:
content = json.loads(fd.read())
except json.JSONDecodeError as err:
print(err)
raise BuildTestError(
f"Unable to read file: {fname}, please make sure its valid json file"
)
return content