Source code for polyvers.utils.fileutil

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015-2018 European Commission (JRC);
# Licensed under the EUPL 1.2+ (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at:
"""Generic utils."""

from pathlib import Path
from typing import Optional, Union
import contextlib
import os
import re

import itertools as itt
import os.path as osp

_file_drive_regex = re.compile(r'^([a-z]):(/)?(.*)$', re.I)
_is_dir_regex = re.compile(r'[^/\\][/\\]$')
_unc_prefix = '\\\\?\\'

[docs]def normpath(path): """Like :func:`osp.normpath()`, but preserving last slash.""" p = osp.normpath(path) if and p[-1] != os.sep: p = p + osp.sep return p
[docs]def abspath(path): """Like :func:`osp.abspath()`, but preserving last slash.""" p = osp.abspath(path) if and p[-1] != os.sep: p = p + osp.sep return p
[docs]def convpath(fpath, abs_path=True, exp_user=True, exp_vars=True): """Without any flags, just pass through :func:`osp.normpath`. """ if exp_user: fpath = osp.expanduser(fpath) if exp_vars: # Mask UNC '\\server\share$\path` from expansion. fpath = fpath.replace('$\\', '_UNC_PATH_HERE_') fpath = osp.expandvars(fpath) fpath = fpath.replace('_UNC_PATH_HERE_', '$\\') fpath = abspath(fpath) if abs_path else normpath(fpath) return fpath
[docs]def ensure_file_ext(fname, ext, *exts, is_regex=False): r""" Ensure that the filepath ends with the extension(s) specified. :param str ext: The 1st extension to search & to append if none matches, so must not be a regex. :param str exts: Other extensions. These may be regexes, depending on `is_regex`; a `'$'` is always added at its end. :param bool is_regex: When true, the rest `exts` are parsed as case-insensitive regexes. Example:: >>> ensure_file_ext('foo', '.bar') '' >>> ensure_file_ext('foo.', '.bar') '' >>> ensure_file_ext('foo.', 'bar') '' >>> ensure_file_ext('foo.BAR', '.bar') 'foo.BAR' >>> ensure_file_ext('foo.DDD', '.bar') '' Note that omitting dot('.') from extension does affect the results:: >>> ensure_file_ext('foo', 'bar') '' >>> ensure_file_ext('foo.BAR', 'bar') 'foo.BAR' >>> ensure_file_ext('fooBAR', 'bar') # File allowed without extension! 'fooBAR' When more extensions are given, the 1st is appended if none matches:: >>> ensure_file_ext('foo.xlt', '.xlsx', '.XLT') 'foo.xlt' >>> ensure_file_ext('foo.xlt', '.xlsx', '.xltx') 'foo.xlt.xlsx' And when regexes:: >>> ensure_file_ext('foo.xlt', '.xlsx', r'\.xl\w{1,2}', is_regex=True) 'foo.xlt' >>> ensure_file_ext('foo.xl^', '.xls', r'\.xl\w{1,2}', is_regex=True) 'foo.xl^.xls' """ if is_regex: ends_with_ext = any( + '$', fname, re.IGNORECASE) for e in (re.escape(ext),) + exts) else: ends_with_ext = fname.lower().endswith(tuple(e.lower() for e in (ext,) + exts)) if not ends_with_ext: if fname.endswith('.'): fname = fname[:-1] if ext.startswith('.'): ext = ext[1:] return '%s.%s' % (fname, ext) return fname
[docs]def ensure_dir_exists(path, mode=0o755): """ensure that a directory exists If it doesn't exist, try to create it and protect against a race condition if another process is doing the same. The default permissions are 755, which differ from os.makedirs default of 777. """ import errno if not os.path.exists(path): try: os.makedirs(path, mode=mode) except OSError as e: if e.errno != errno.EEXIST: raise elif not os.path.isdir(path): raise OSError("%r exists but is not a directory" % path)
[docs]def find_git_root(path=None) -> Optional[Path]: """ Search dirs up for a Git-repo like ``git rev-parse --show-toplevel``. :param path: where to start searching from, `cwd` if not given. :return: a `pathlib` native path, or None """ if not path: cwd = Path() for f in itt.chain([cwd], cwd.resolve().parents): if (f / '.git').is_dir(): return f
# raise pvtags.GitVoidError( # "Current-dir '%s' is not within a git repository!" % Path.cwd()) ################### ## pathlib.Paths ## ################### ##TODO: publicize path-set ops. def _is_base_or_same(basepath: Path, longpath: Path, strict=False) -> Union[bool, None]: """ Inverses, resolving, same-file checking, non-raising :meth:`pathlib.relative_to()`. :return: A 3-state bool denoting relativeness, or None the when they are the same; non-existing file(s) are still checked for relativeness BUT when not resolved, them surprises happen, check TCs! (e.g. 'BAD' considered base of 'BAD/..', but not on Linux...) .. Tip:: To check if `longpath` has or `basepath` use:: _is_base_or_same(obase, long) in (None, True) """ try: rel = longpath.resolve(strict=strict).relative_to(basepath.resolve(strict=strict)) return None if str(rel) == '.' else bool(rel) except (ValueError, FileNotFoundError): try: rel = longpath.relative_to(basepath) return None if str(rel) == '.' else bool(rel) except ValueError: return False ##TODO: publicize path-set ops. def _is_same_file(fp1: Path, fp2: Path) -> Union[bool, None]: """ Non-raising version of pathlib. :return: a 3-state bool denoting sameness, or None if non-existing file(s) involved. """ try: return fp1.samefile(fp2) except FileNotFoundError: return None @contextlib.contextmanager def chdir(path): opath = os.getcwd() try: os.chdir(path) yield finally: os.chdir(opath)