# FIXME: port properly! """Shared functionality. Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports from collections import namedtuple import logging import os import pkgutil import platform import re import typing import warnings # exports __all__: typing.Sequence[str] = ( 'Resolution', 'Struct', 'clamp', 'fileopen', 'flatten', 'fst', 'import_all', 'is_hex', 'is_list', 'magnitude_fmt', 'truncate_dir', 'get_root', ) ## code ## logger = logging.getLogger(__name__) fst = lambda lst: lst[0] def is_list(cand): """Return true if *cand* is a list, a set, or a tuple""" return isinstance(cand, list) or isinstance(cand, set) or isinstance(cand, tuple) def import_all(module, exclude=None, verbose=False): """Recursively import all submodules of *module*. *exclude* is a set of submodule names which will be omitted. With *verbose*, all imports are logged with level info. Returns all imported modules. >>> import tagit >>> import_all(tagit, exclude={'tagit.shared.external'}) """ exclude = set([] if exclude is None else exclude) imports = [] for importer, name, ispkg in pkgutil.iter_modules(module.__path__, module.__name__ + '.'): if ispkg and all(re.match(exl, name) is None for exl in exclude): if verbose: logger.info(f'importing: {name}') try: module = __import__(name, fromlist='dummy') imports.append(module) imports += import_all(module, exclude, verbose) except Exception as e: logger.error(f'importing: {name}') return imports def clamp(value, hi, lo=0): """Restrain a *value* to the range *lo* to *hi*.""" return max(lo, min(hi, value)) Resolution = namedtuple('resolution', ('width', 'height')) def truncate_dir(path, cutoff=3): """Remove path up to last *cutoff* directories""" if cutoff < 0: raise ValueError('path cutoff must be positive') dirs = os.path.dirname(path).split(os.path.sep) last_dirs = dirs[max(0, len(dirs) - cutoff):] prefix = '' if os.path.isabs(path) and len(last_dirs) == len(dirs): prefix = os.path.sep return prefix + os.path.join(*(last_dirs + [os.path.basename(path)])) def magnitude_fmt(num, suffix='iB', scale=1024): """Human-readable number format. adapted from Sridhar Ratnakumar, 2009 https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size """ for unit in ['','K','M','G','T','P','E','Z']: if abs(num) < scale: return "%3.1f%s%s" % (num, unit, suffix) num /= scale return "%.1f%s%s" % (num, 'Y', suffix) class Struct(dict): """Dict with item access as members. >>> tup = Struct(timestamp=123, series=['1','2','3']) >>> tup.timestamp 123 >>> tup['timestamp'] 123 """ def __getattr__(self, name): return self[name] def __setattr__(self, name, value): self[name] = value def flatten(lst): flat = [] for itm in lst: flat.extend(list(itm)) return flat def is_hex(string): """Return True if the *string* can be interpreted as a hex value.""" try: int(string, 16) return True except ValueError: return False except TypeError: return False def fileopen(pth): """Open a file in the preferred application as a subprocess. This operation is platform dependent. """ try: binopen = { "Linux" : "xdg-open", # Linux "darwin" : "open", # MAX OS X "Windows" : "start", # Windows }.get(platform.system()) subprocess.call((binopen, pth)) except KeyError: warnings.warn('Unknown platform {}'.format(platform.system())) def get_root(obj): """Traverse the widget tree upwards until the root is found.""" while obj.parent is not None and obj.parent != obj.parent.parent: if hasattr(obj, 'root') and obj.root is not None: return obj.root obj = obj.parent return obj ## EOF ##