Skip to content

file_utils

trestle.common.file_utils ¤

Trestle file system utils.

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

Functions¤

check_oscal_directories(root_path) ¤

Identify the state of the trestle workspace.

Traverses trestle workspace and looks for unexpected files or directories. Additional files are allowed in the Trestle root but not inside the model folders.

Source code in trestle/common/file_utils.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def check_oscal_directories(root_path: pathlib.Path) -> bool:
    """
    Identify the state of the trestle workspace.

    Traverses trestle workspace and looks for unexpected files or directories.
    Additional files are allowed in the Trestle root but not inside the model folders.
    """
    trestle_dir_walk = os.walk(root_path)
    is_valid = True

    for _, dirs, _ in trestle_dir_walk:
        for d in dirs:
            if d in MODEL_DIR_LIST:
                is_valid = _verify_oscal_folder(root_path / d)
                if not is_valid:
                    break
    return is_valid

extract_project_model_path(path) ¤

Get the base path of the trestle model project.

Source code in trestle/common/file_utils.py
201
202
203
204
205
206
207
208
def extract_project_model_path(path: pathlib.Path) -> Optional[pathlib.Path]:
    """Get the base path of the trestle model project."""
    if len(path.parts) > 2:
        for i in range(2, len(path.parts)):
            current = pathlib.Path(path.parts[0]).joinpath(*path.parts[1:i + 1])
            if _is_valid_project_model_path(current):
                return current
    return None

extract_trestle_project_root(path) ¤

Get the trestle workspace root folder in the path.

Source code in trestle/common/file_utils.py
180
181
182
183
184
185
186
def extract_trestle_project_root(path: pathlib.Path) -> Optional[pathlib.Path]:
    """Get the trestle workspace root folder in the path."""
    while len(path.parts) > 1:  # it must not be the system root directory
        if is_valid_project_root(path):
            return path
        path = path.parent
    return None

get_contextual_file_type(path) ¤

Return the file content type for files in the given directory, if it's a trestle workspace.

Source code in trestle/common/file_utils.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def get_contextual_file_type(path: pathlib.Path) -> FileContentType:
    """Return the file content type for files in the given directory, if it's a trestle workspace."""
    if not _is_valid_project_model_path(path):
        raise err.TrestleError(f'Trestle workspace not found at path {path}')

    for file_or_directory in iterdir_without_hidden_files(path):
        if file_or_directory.is_file():
            return FileContentType.to_content_type(file_or_directory.suffix)

    for file_or_directory in path.iterdir():
        if file_or_directory.is_dir():
            return get_contextual_file_type(file_or_directory)

    raise err.TrestleError('No files found in the project.')

insert_text_in_file(file_path, tag, text) ¤

Insert text lines after line containing tag.

Return True on success, False tag not found. Text is a string with appropriate \n line endings. If tag is none just add at end of file. This will only open file once if tag is not found.

Source code in trestle/common/file_utils.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def insert_text_in_file(file_path: pathlib.Path, tag: Optional[str], text: str) -> bool:
    r"""Insert text lines after line containing tag.

    Return True on success, False tag not found.
    Text is a string with appropriate \n line endings.
    If tag is none just add at end of file.
    This will only open file once if tag is not found.
    """
    if not file_path.exists():
        raise TrestleError(f'Test file {file_path} not found.')
    if tag:
        lines: List[str] = []
        with file_path.open('r', encoding=const.FILE_ENCODING) as f:
            lines = f.readlines()
        for ii, line in enumerate(lines):
            if line.find(tag) >= 0:
                lines.insert(ii + 1, text)
                with file_path.open('w', encoding=const.FILE_ENCODING) as f:
                    f.writelines(lines)
                return True
    else:
        with file_path.open('a', encoding=const.FILE_ENCODING) as f:
            f.writelines(text)
        return True
    return False

is_directory_name_allowed(name) ¤

Determine whether a directory name, which is a 'non-core-OSCAL activity/directory is allowed.

Parameters:

Name Type Description Default
name str

the name which is assumed may take the form of a relative path for task/subtasks.

required

Returns:

Type Description
bool

Whether the name is allowed or not allowed (interferes with assumed project directories such as catalogs).

Source code in trestle/common/file_utils.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def is_directory_name_allowed(name: str) -> bool:
    """Determine whether a directory name, which is a 'non-core-OSCAL activity/directory is allowed.

    args:
        name: the name which is assumed may take the form of a relative path for task/subtasks.

    Returns:
        Whether the name is allowed or not allowed (interferes with assumed project directories such as catalogs).
    """
    # Task must not use an OSCAL directory
    # Task must not self-interfere with a project
    pathed_name = pathlib.Path(name)

    root_path = pathed_name.parts[0]
    if root_path in const.MODEL_TYPE_TO_MODEL_DIR.values():
        logger.warning('Task name is the same as an OSCAL schema name.')
        return False
    if root_path[0] == '.':
        logger.warning('Task name must not start with "."')
        return False
    if pathed_name.suffix != '':
        # Does it look like a file
        logger.warning('Task name must not look like a file path (e.g. contain a suffix')
        return False
    if '__global__' in pathed_name.parts:
        logger.warning('Task name cannot contain __global__')
        return False
    return True

is_hidden(file_path) ¤

Determine whether a file is hidden based on the appropriate os attributes.

This function will only work for the current file path only (e.g. not if a parent is hidden).

Parameters:

Name Type Description Default
file_path Path

The file path for which we are testing whether the file / directory is hidden.

required

Returns:

Type Description
bool

Whether or not the file is file/directory is hidden.

Source code in trestle/common/file_utils.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def is_hidden(file_path: pathlib.Path) -> bool:
    """
    Determine whether a file is hidden based on the appropriate os attributes.

    This function will only work for the current file path only (e.g. not if a parent is hidden).

    Args:
        file_path: The file path for which we are testing whether the file / directory is hidden.

    Returns:
        Whether or not the file is file/directory is hidden.
    """
    # as far as trestle is concerned all .* files are hidden even on windows, regardless of attributes
    if file_path.stem.startswith('.'):
        return True
    # Handle windows
    if is_windows():  # pragma: no cover
        attribute = win32api.GetFileAttributes(str(file_path))
        return attribute & (win32con.FILE_ATTRIBUTE_HIDDEN | win32con.FILE_ATTRIBUTE_SYSTEM)
    return False

is_local_and_visible(file_path) ¤

Is the file or dir local (not a symlink) and not hidden.

Source code in trestle/common/file_utils.py
89
90
91
def is_local_and_visible(file_path: pathlib.Path) -> bool:
    """Is the file or dir local (not a symlink) and not hidden."""
    return not (is_hidden(file_path) or is_symlink(file_path))

Is the file path a symlink.

Source code in trestle/common/file_utils.py
82
83
84
85
86
def is_symlink(file_path: pathlib.Path) -> bool:
    """Is the file path a symlink."""
    if is_windows():
        return file_path.suffix == '.lnk'
    return file_path.is_symlink()

is_valid_project_root(path) ¤

Check if the path is a valid trestle workspace root.

Source code in trestle/common/file_utils.py
174
175
176
177
def is_valid_project_root(path: pathlib.Path) -> bool:
    """Check if the path is a valid trestle workspace root."""
    trestle_dir = path / const.TRESTLE_CONFIG_DIR
    return trestle_dir.exists() and trestle_dir.is_dir()

is_windows() ¤

Check if current operating system is Windows.

Source code in trestle/common/file_utils.py
40
41
42
def is_windows() -> bool:
    """Check if current operating system is Windows."""
    return platform.system() == const.WINDOWS_PLATFORM_STR

iterdir_without_hidden_files(directory_path) ¤

Get iterator over all paths in the given directory_path excluding hidden files.

Parameters:

Name Type Description Default
directory_path Path

The directory to iterate through.

required

Returns:

Type Description
Iterable[Path]

Iterator over the files in the directory excluding hidden files.

Source code in trestle/common/file_utils.py
45
46
47
48
49
50
51
52
53
54
55
56
57
def iterdir_without_hidden_files(directory_path: pathlib.Path) -> Iterable[pathlib.Path]:
    """
    Get iterator over all paths in the given directory_path excluding hidden files.

    Args:
        directory_path: The directory to iterate through.

    Returns:
        Iterator over the files in the directory excluding hidden files.
    """
    filtered_paths = list(filter(lambda p: not is_hidden(p) or p.is_dir(), pathlib.Path.iterdir(directory_path)))

    return filtered_paths.__iter__()

load_file(file_path) ¤

Load JSON or YAML file content into a dict.

This is not intended to be the default load mechanism. It should only be used if a OSCAL object type is unknown but the context a user is in.

Source code in trestle/common/file_utils.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def load_file(file_path: pathlib.Path) -> Dict[str, Any]:
    """
    Load JSON or YAML file content into a dict.

    This is not intended to be the default load mechanism. It should only be used
    if a OSCAL object type is unknown but the context a user is in.
    """
    content_type = FileContentType.to_content_type(file_path.suffix)
    with file_path.open('r', encoding=const.FILE_ENCODING) as f:
        if content_type == FileContentType.YAML:
            yaml = YAML(typ='safe')
            return yaml.load(f)
        if content_type == FileContentType.JSON:
            return json.load(f)
    return {}

make_hidden_file(file_path) ¤

Make hidden file.

Source code in trestle/common/file_utils.py
124
125
126
127
128
129
130
131
132
def make_hidden_file(file_path: pathlib.Path) -> None:
    """Make hidden file."""
    if not file_path.name.startswith('.') and not is_windows():
        file_path = file_path.parent / ('.' + file_path.name)

    file_path.touch()
    if is_windows():
        atts = win32api.GetFileAttributes(str(file_path))
        win32api.SetFileAttributes(str(file_path), win32con.FILE_ATTRIBUTE_HIDDEN | atts)

prune_empty_dirs(file_path, pattern) ¤

Remove directories with no subdirs and with no files matching pattern.

Source code in trestle/common/file_utils.py
299
300
301
302
303
304
305
306
307
def prune_empty_dirs(file_path: pathlib.Path, pattern: str) -> None:
    """Remove directories with no subdirs and with no files matching pattern."""
    deleted: Set[str] = set()
    # this traverses from leaf nodes upward so only needs one traversal
    for current_dir, subdirs, _ in os.walk(str(file_path), topdown=False):
        true_dirs = [subdir for subdir in subdirs if os.path.join(current_dir, subdir) not in deleted]
        if not true_dirs and not any(glob.glob(f'{current_dir}/{pattern}')):
            shutil.rmtree(current_dir)
            deleted.add(current_dir)

relative_resolve(candidate, cwd) ¤

Resolve a candidate file path relative to a provided cwd.

This is to circumvent bad behaviour for resolve on windows platforms where the path must exist.

If a relative dir is passed it presumes the directory is relative to the PROVIDED cwd. If relative expansions exist (e.g. ../) the final result must still be within the cwd.

If an absolute path is provided it tests whether the path is within the cwd or not.

Source code in trestle/common/file_utils.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def relative_resolve(candidate: pathlib.Path, cwd: pathlib.Path) -> pathlib.Path:
    """Resolve a candidate file path relative to a provided cwd.

    This is to circumvent bad behaviour for resolve on windows platforms where the path must exist.

    If a relative dir is passed it presumes the directory is relative to the PROVIDED cwd.
    If relative expansions exist (e.g. ../) the final result must still be within the cwd.

    If an absolute path is provided it tests whether the path is within the cwd or not.

    """
    # Expand user first if applicable.
    candidate = candidate.expanduser()

    if not cwd.is_absolute():
        raise TrestleError('Error handling current working directory. CWD is expected to be absolute.')

    if not candidate.is_absolute():
        new = pathlib.Path(cwd / candidate).resolve()
    else:
        new = candidate.resolve()
    try:
        new.relative_to(cwd)
    except ValueError:
        raise TrestleError(f'Provided dir {candidate} is not relative to {cwd}')
    return new

handler: python