#!python3
"""System info and decorators.
.. warning:: This module must not introduce dependency on any other Black Mamba
modules and must be importable on any other platform as well.
"""
import sys
import traceback
import functools
try:
import console
except ImportError:
console = None
# 3.1, 301016
# 3.1.1 beta, 311008
PYTHONISTA = sys.platform == 'ios'
"""bool: True if we're running within Pythonista or False."""
PYTHONISTA_VERSION = None
"""str: Pythonista version or `None` if we're not within Pythonista."""
PYTHONISTA_BUNDLE_VERSION = None
"""int: Pythonista bundle version or `None` if we're not within Pythonista."""
PYTHONISTA_VERSION_TUPLE = None
"""tuple(int): Pythonista version tuple (3, 1, 1) or `None` if we're not within Pythonista."""
IOS = sys.platform == 'ios'
"""bool: `True` if we're running within iOS or `False`."""
IOS_VERSION = None
"""str: iOS version or `None` if we're not within iOS."""
IOS_VERSION_TUPLE = None
"""tuple(int): iOS version tuple (11, 0) or `None` if we're not within iOS."""
def _version_tuple(version):
if not version:
return None
return tuple(map(int, (version.split('.'))))
if PYTHONISTA:
import plistlib
import os
try:
plist_path = os.path.abspath(os.path.join(sys.executable, '..', 'Info.plist'))
plist = plistlib.readPlist(plist_path)
PYTHONISTA_VERSION = plist['CFBundleShortVersionString']
PYTHONISTA_BUNDLE_VERSION = int(plist['CFBundleVersion'])
PYTHONISTA_VERSION_TUPLE = _version_tuple(PYTHONISTA_VERSION)
except Exception:
pass
if IOS:
try:
from objc_util import ObjCClass
IOS_VERSION = str(ObjCClass('UIDevice').currentDevice().systemVersion())
IOS_VERSION_TUPLE = _version_tuple(IOS_VERSION)
except Exception:
pass
class _Available:
def __init__(self, from_version=None, to_version=None):
if from_version and to_version:
raise ValueError('Either from_version or to_version can be provided, not both')
self._from_version = _version_tuple(from_version)
self._to_version = _version_tuple(to_version)
def version(self):
raise Exception('Not implemented, return version as tuple(int)')
def _available(self):
current_version = self.version()
if not current_version:
return False
if self._to_version:
return current_version <= self._to_version
if self._from_version:
return current_version >= self._from_version
return True
def __call__(self, fn, *args, **kwargs):
def func(*args, **kwargs):
if self._available():
return fn(*args, **kwargs)
return None
return func
[docs]class iOS(_Available):
"""Decorator to execute function under specific iOS versions.
Return value is return value of decorated function or `None`
if iOS condition isn't met.
Examples:
Run function only within any iOS version::
@iOS()
def run_me():
pass
Run function only within iOS >= 11.0::
@iOS('11.0') # or @iOS(from_version='11.0')
def run_me():
pass
Run function only within iOS <= 11.0::
@iOS(None, '11.0') # or @iOS(to_version='11.0')
def run_me():
pass
"""
def version(self):
return IOS_VERSION_TUPLE
[docs]class Pythonista(_Available):
"""Decorator to execute function under specific Pythonista versions.
By default, function is not executed under application extension.
You have to pass ``appex=True`` if you'd like to run some function
under appex as well.
Return value is return value of decorated function or `None`
if Pythonista condition isn't met.
Examples:
Run function only within any Pythonista version::
@Pythonista()
def run_me():
pass
Run function only within any Pythonista version and allow appex::
@Pythonista(appex=True)
def run_me():
pass
Run function only within any Pythonista version and disallow appex::
@Pythonista(appex=False)
def run_me():
pass
Run function only within Pythonista >= 3.1.1::
@Pythonista('3.1.1') # or @Pythonista(from_version='3.1.1')
def run_me():
pass
Run function only within Pythonista <= 3.2::
@Pythonista(None, '3.2') # or @Pythonista(to_version='3.2')
def run_me():
pass
"""
def __init__(self, from_version=None, to_version=None, appex=None):
super().__init__(from_version, to_version)
self._appex = appex
def _available(self):
available = super()._available()
if available and self._appex is not None:
import appex
available = appex.is_running_extension() == self._appex
return available
def version(self):
return PYTHONISTA_VERSION_TUPLE
[docs]def catch_exceptions(func):
"""Decorator catching all exceptions and printing info to the console.
Use this decorator for functions handling keyboard shortcuts,
keyboard events, ... to avoid Pythonista crash.
Args:
func: Function to decorate
Returns:
Return value of decorated function.
"""
@functools.wraps(func)
def new_func(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception:
if console:
console.set_color(1, 0, 0)
print(traceback.format_exc())
print('Please, file an issue at {}'.format('https://github.com/zrzka/blackmamba/issues'))
if console:
console.set_color()
return new_func