Skip to content

Files

  • File Utilities

data_types: dict[str, DataFileType] = {'toml': {'load': toml_load, 'load_kw': {}, 'dump': toml_dump, 'dump_kw': {'sort_keys': True}}, 'yaml': {'load': yaml_load, 'load_kw': {'Loader': yamlLoader}, 'dump': yaml_dump, 'dump_kw': {'Dumper': yamlDumper, 'sort_keys': True, 'indent': 2, 'allow_unicode': True}}, 'json': {'load': json.load, 'load_kw': {}, 'dump': json.dump, 'dump_kw': {'sort_keys': True, 'indent': 2, 'ensure_ascii': False}}} module-attribute

  • File Info Utils

DataFileType

Bases: TypedDict

Data file type (json, toml, yaml, etc).

Source code in src/utils/files.py
32
33
34
35
36
37
class DataFileType (TypedDict):
    """Data file type (json, toml, yaml, etc)."""
    load: Callable
    dump: Callable
    load_kw: dict[str, Union[Callable, bool, str]]
    dump_kw: dict[str, Union[Callable, bool, str]]

check_valid_file(path, ext=None)

Checks if a file path provided exists, optionally validate an extension type. @param path: Path to the file to verify. @param ext: Extension to check, if provided. @return: True if file is valid, otherwise False.

Source code in src/utils/files.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def check_valid_file(path: Union[str, os.PathLike], ext: Optional[str] = None) -> bool:
    """
    Checks if a file path provided exists, optionally validate an extension type.
    @param path: Path to the file to verify.
    @param ext: Extension to check, if provided.
    @return: True if file is valid, otherwise False.
    """
    with suppress(Exception):
        check = str(path).lower()
        if osp.isfile(check):
            if ext:
                ext = (ext if ext.startswith('.') else f'.{ext}').lower()
                if not check.endswith(ext):
                    return False
            return True
    return False

copy_config_or_verify(path_from, path_to, data_file)

Copy one config to another, or verify it if it exists. @param path_from: Path to the file to be copied. @param path_to: Path to the file to create, if it doesn't exist. @param data_file: Data schema file to use for validating an existing INI file.

Source code in src/utils/files.py
281
282
283
284
285
286
287
288
289
290
def copy_config_or_verify(path_from: Path, path_to: Path, data_file: Path) -> None:
    """
    Copy one config to another, or verify it if it exists.
    @param path_from: Path to the file to be copied.
    @param path_to: Path to the file to create, if it doesn't exist.
    @param data_file: Data schema file to use for validating an existing INI file.
    """
    if osp.isfile(path_to):
        return verify_config_fields(path_to, data_file)
    shutil.copy(path_from, path_to)

dump_data_file(obj, data_file, config=None)

Dump object to a data file. @param obj: Iterable or dict object to save to data file. @param data_file: Path to the data file to be dumps. @param config: Dict data to modify DataFileType configuration for this data dump procedure. @raise ValueError: If data file type not supported. @raise OSError: If dumping to data file fails.

Source code in src/utils/files.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def dump_data_file(
    obj: Union[list, dict, tuple, set],
    data_file: Union[str, os.PathLike],
    config: Optional[dict] = None
) -> None:
    """
    Dump object to a data file.
    @param obj: Iterable or dict object to save to data file.
    @param data_file: Path to the data file to be dumps.
    @param config: Dict data to modify DataFileType configuration for this data dump procedure.
    @raise ValueError: If data file type not supported.
    @raise OSError: If dumping to data file fails.
    """
    data_type = Path(data_file).suffix[1:]
    parser: DataFileType = data_types.get(data_type, {})
    if not parser:
        raise ValueError("Data file provided does not match a supported data file type.\n"
                         f"Types supported: {', '.join(data_types.keys())}\n"
                         f"Type received: {data_type}")
    if config:
        parser.update(config)
    with util_file_lock:
        with open(data_file, 'w', encoding='utf-8') as f:
            try:
                parser['dump'](obj, f, **parser['dump_kw'])
            except Exception as e:
                raise OSError(f"Unable to dump data to data file:\n{data_file}") from e

ensure_path_exists(path)

Ensure that directories in path exists. @param path: Folder path to check and create if necessary.

Source code in src/utils/files.py
341
342
343
344
345
346
def ensure_path_exists(path: Union[str, os.PathLike]) -> None:
    """
    Ensure that directories in path exists.
    @param path: Folder path to check and create if necessary.
    """
    Path(osp.dirname(path)).mkdir(mode=711, parents=True, exist_ok=True)

get_config_object(path)

Returns a ConfigParser object using a valid ini path. @param path: Path to ini config file. @return: ConfigParser object. @raise: ValueError if valid ini file wasn't received.

Source code in src/utils/files.py
305
306
307
308
309
310
311
312
313
314
315
def get_config_object(path: Union[str, os.PathLike, list[Union[str, os.PathLike]]]) -> ConfigParser:
    """
    Returns a ConfigParser object using a valid ini path.
    @param path: Path to ini config file.
    @return: ConfigParser object.
    @raise: ValueError if valid ini file wasn't received.
    """
    config = ConfigParser(allow_no_value=True)
    config.optionxform = str
    config.read(path, encoding='utf-8')
    return config

get_file_size_mb(file_path, decimal=1)

Get a file's size in megabytes rounded. @param file_path: Path to the file. @param decimal: Number of decimal places to allow when rounding. @return: Float representing the filesize in megabytes rounded.

Source code in src/utils/files.py
77
78
79
80
81
82
83
84
def get_file_size_mb(file_path: Union[str, os.PathLike], decimal: int = 1) -> float:
    """
    Get a file's size in megabytes rounded.
    @param file_path: Path to the file.
    @param decimal: Number of decimal places to allow when rounding.
    @return: Float representing the filesize in megabytes rounded.
    """
    return round(os.path.getsize(file_path) / (1024 * 1024), decimal)

get_kivy_config_from_schema(config)

Return valid JSON data for use with Kivy settings panel. @param config: Path to config schema file, JSON or TOML. @return: Json string dump of validated data.

Source code in src/utils/files.py
266
267
268
269
270
271
272
273
274
275
276
277
278
def get_kivy_config_from_schema(config: Path) -> str:
    """
    Return valid JSON data for use with Kivy settings panel.
    @param config: Path to config schema file, JSON or TOML.
    @return: Json string dump of validated data.
    """
    # Need to load data as JSON
    raw = load_data_file(data_file=config)

    # Use correct parser
    if config.suffix == '.toml':
        raw = parse_kivy_config_toml(raw)
    return json.dumps(parse_kivy_config_json(raw))

get_unique_filename(path, name, ext, suffix)

If a filepath exists, number the file according to the lowest number that doesn't exist. @param path: Path to the file. @param name: Name of the file. @param ext: Extension of the file. @param suffix: Suffix to add before the number. @return: Unique filename.

Source code in src/utils/files.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def get_unique_filename(path: Union[str, os.PathLike], name: str, ext: str, suffix: str) -> str:
    """
    If a filepath exists, number the file according to the lowest number that doesn't exist.
    @param path: Path to the file.
    @param name: Name of the file.
    @param ext: Extension of the file.
    @param suffix: Suffix to add before the number.
    @return: Unique filename.
    """
    num = 0
    new_name = f"{name} ({suffix})" if suffix else name
    suffix = f' ({suffix}'+' {})' if suffix else ' ({})'
    while Path(path, f"{new_name}{ext}").is_file():
        num += 1
        new_name = f"{name}{suffix.format(num)}"
    return new_name

load_data_file(data_file, config=None)

Load object from a data file. @param data_file: Path to the data file to be loaded. @param config: Dict data to modify DataFileType configuration for this data load procedure. @return: Iterable or dict object loaded from data file. @raise ValueError: If data file type not supported. @raise OSError: If loading data file fails.

Source code in src/utils/files.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def load_data_file(
    data_file: Union[str, os.PathLike],
    config: Optional[dict] = None
) -> Union[list, dict, tuple, set]:
    """
    Load object from a data file.
    @param data_file: Path to the data file to be loaded.
    @param config: Dict data to modify DataFileType configuration for this data load procedure.
    @return: Iterable or dict object loaded from data file.
    @raise ValueError: If data file type not supported.
    @raise OSError: If loading data file fails.
    """
    data_type = Path(data_file).suffix[1:]
    parser: DataFileType = data_types.get(data_type, {})
    if not parser:
        raise ValueError("Data file provided does not match a supported data file type.\n"
                         f"Types supported: {', '.join(data_types.keys())}\n"
                         f"Type received: {data_type}")
    if config:
        parser.update(config)
    with util_file_lock:
        with open(data_file, 'r', encoding='utf-8') as f:
            try:
                return parser['load'](f, **parser['load_kw']) or {}
            except Exception as e:
                raise OSError(f"Unable to load data from data file:\n{data_file}") from e

parse_kivy_config_json(raw)

Parse config JSON data for use with Kivy settings panel. @param raw: Raw loaded JSON data. @return: Properly parsed data safe for use with Kivy.

Source code in src/utils/files.py
206
207
208
209
210
211
212
213
214
215
216
def parse_kivy_config_json(raw: list[dict]) -> list[dict]:
    """
    Parse config JSON data for use with Kivy settings panel.
    @param raw: Raw loaded JSON data.
    @return: Properly parsed data safe for use with Kivy.
    """
    # Remove unsupported keys
    for row in raw:
        if 'default' in row:
            row.pop('default')
    return raw

parse_kivy_config_toml(raw)

Parse config TOML data for use with Kivy settings panel. @param raw: Raw loaded TOML data. @return: Properly parsed data safe for use with Kivy.

Source code in src/utils/files.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def parse_kivy_config_toml(raw: dict) -> list[dict]:
    """
    Parse config TOML data for use with Kivy settings panel.
    @param raw: Raw loaded TOML data.
    @return: Properly parsed data safe for use with Kivy.
    """

    # Process __CONFIG__ header if present
    cfg_header = raw.pop('__CONFIG__', {})
    prefix = cfg_header.get('prefix', '')

    # Process data
    data: list[dict] = []
    for section, settings in raw.items():

        # Add section title if it exists
        if title := settings.pop('title', None):
            data.append({
                'type': 'title',
                'title': title
            })

        # Add each setting within this section
        for key, field in settings.items():

            # Establish data type and default value
            data_type = field.get('type', 'bool')
            display_default = default = field.get('default', 0)
            if data_type == 'bool':
                display_default = 'True' if default else 'False'
            elif data_type in ['string', 'options', 'path']:
                display_default = f"'{default}'"
            setting = {
                'type': data_type,
                'title': msg_bold(field.get('title', 'Broken Setting')),
                'desc': f"{field.get('desc', '')}\n"
                        f"{msg_bold(f'(Default: {display_default})')}",
                'section': f'{prefix}.{section}' if prefix else section,
                'key': key, 'default': default}
            if options := field.get('options'):
                setting['options'] = options
            data.append(setting)

    # Return parsed data
    return data

remove_config_file(ini_file)

Check if config file exists, then remove it. @return: True if removed, False if not.

Source code in src/utils/files.py
293
294
295
296
297
298
299
300
301
302
def remove_config_file(ini_file: str) -> bool:
    """
    Check if config file exists, then remove it.
    @return: True if removed, False if not.
    """
    if osp.isfile(ini_file):
        with suppress(Exception):
            remove(ini_file)
            return True
    return False

verify_config_fields(ini_file, data_file)

Validate that all settings fields present in a given json data are present in config file. If any are missing, add them and return @param ini_file: Config file to verify contains the proper fields. @param data_file: Data file containing config fields to check for, JSON or TOML.

Source code in src/utils/files.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def verify_config_fields(ini_file: Path, data_file: Path) -> None:
    """
    Validate that all settings fields present in a given json data are present in config file.
    If any are missing, add them and return
    @param ini_file: Config file to verify contains the proper fields.
    @param data_file: Data file containing config fields to check for, JSON or TOML.
    """
    # Track data and changes
    data, changed = {}, False

    # Data file doesn't exist or is unsupported data type
    if not data_file.is_file() or data_file.suffix not in ['.toml', '.json']:
        return

    # Load data from JSON or TOML file
    raw = load_data_file(data_file)
    raw = parse_kivy_config_toml(raw) if data_file.suffix == '.toml' else raw

    # Ensure INI file exists and load ConfigParser
    ensure_path_exists(ini_file)
    config = get_config_object(ini_file)

    # Build a dictionary of the necessary values
    for row in raw:
        # Add row if it's not a title
        if row.get('type', 'title') == 'title':
            continue
        data.setdefault(
            row.get('section', 'BROKEN'), []
        ).append({
            'key': row.get('key', ''),
            'value': row.get('default', 0)
        })

    # Add the data to ini where missing
    for section, settings in data.items():
        # Check if the section exists
        if not config.has_section(section):
            config.add_section(section)
            changed = True
        # Check if each setting exists
        for setting in settings:
            if not config.has_option(section, setting['key']):
                config.set(section, setting['key'], str(setting['value']))
                changed = True

    # If ini has changed, write changes
    if changed:
        with open(ini_file, "w", encoding="utf-8") as f:
            config.write(f)