# coding: utf-8
"""Classes used to execute a visualizer within the Python interpreter."""
from __future__ import annotations
import sys
import os
import abc
from shutil import which
from monty.termcolor import cprint
from monty.functools import lazy_property
__all__ = [
"Visualizer",
]
def is_macosx() -> bool:
"""True if we are running on Mac."""
return "darwin" in sys.platform
def find_loc(app_name) -> str:
"""
Returns the location of the application from its name. None if not found.
"""
path = _find_loc(app_name)
if path is not None: return path
path = _find_loc(app_name.upper())
return path
def _find_loc(app_name) -> str: # pragma: no cover
# Try command line version
path = which(app_name)
if path is not None: return path
# Treat Mac OsX applications.
if is_macosx():
system_apps = [f.lower() for f in os.listdir("/Applications")]
try:
i = system_apps.index(app_name)
return os.path.join("/Applications", system_apps[i])
except ValueError:
try:
i = system_apps.index(app_name + ".app")
return os.path.join("/Applications", system_apps[i] + ".app")
except ValueError:
pass
try:
user_dir = os.path.expanduser("~/Applications/")
user_apps = [f.lower() for f in os.listdir(user_dir)]
try:
i = user_apps.index(app_name)
return os.path.join(user_dir, user_apps[i])
except ValueError:
try:
i = user_apps.index(app_name + ".app")
return os.path.join("/Applications", user_apps[i] + ".app")
except ValueError:
pass
except Exception:
pass
return None
class VisualizerError(Exception):
"""Base class for Visualizer errors"""
[docs]
class Visualizer(metaclass=abc.ABCMeta):
"""
Handle the visualization of data.
"""
# True if its a Mac OsX applications (default is unix executable).
# If we have a Mac OsX application we have to run it with "open -a app_name --args"
is_macosx_app = False
Error = VisualizerError
def __init__(self, filepath: str):
"""
Args:
filepath: Name of the file to visualize
"""
self.filepath = os.path.abspath(filepath)
def __str__(self):
return "%s: %s, is_macosx_app %s, filepath: %s" % (
self.__class__.__name__, self.binpath, self.is_macosx_app, self.filepath)
def __call__(self): # pragma: no cover
"""
Call the visualizer in a subprocess.
Returns: exit status of the subprocess.
"""
from subprocess import call
if not self.is_macosx_app:
cprint("Executing: binpath=%s, cmdarg=%s, filepath=%s" % (self.binpath, self.cmdarg, self.filepath), "yellow")
if self.binpath is None:
raise RuntimeError("binpath is None, please make sure that executable can be found in $PATH")
return call([self.binpath, self.cmdarg, self.filepath])
else:
# Mac-OSx applications can be launched with `open -a Vesta --args si.cif`
cmd = "open -a %s --args %s %s" % (self.name, self.cmdarg, self.filepath)
cprint("Executing MacOSx open command: %s" % cmd, "yellow")
return call(cmd, shell=True)
@property
def cmdarg(self):
"""Arguments that must be used to visualize the file."""
root, ext = os.path.splitext(self.filepath)
ext = ext.replace(".", "")
#print(root, ext)
for e, args in self.EXTS:
if e == ext:
return args
return " "
[docs]
@lazy_property
def binpath(self) -> str:
"""Absolute path of the binary. None if app is not found"""
return find_loc(self.name)
@property
def is_available(self) -> bool:
"""True is the visualizer is available on the local machine."""
return self.binpath is not None
[docs]
@classmethod
def get_available(cls, ext=None) -> list:
"""
List of visualizers available on the local host.
If ext is not None, only the visualizers supporting this extension are returned.
"""
visus = [v for v in cls.__subclasses__() if v.is_available]
if ext is None: return visus
return [v for v in visus if v.support_ext(ext)]
[docs]
@classmethod
def support_ext(cls, ext) -> bool:
"""True if visualizer supports the extension ext."""
if ext.startswith("."): ext = ext[1:]
return any(e == ext for e, _ in cls.EXTS)
[docs]
@classmethod
def from_file(cls, filepath: str):
"""
Initialize a subclass of :class:`Visualizer` from filepath, the application
is chosen automatically depending on the file extension.
Raise:
`VisualizerError` if no visualizer is available for the given file.
"""
# Get the file extension.
root, ext = os.path.splitext(filepath)
if not ext:
raise ValueError("Cannot detect file extension in %s " % filepath)
avail_visus = cls.get_available(ext=ext)
if not avail_visus:
raise cls.Error("Cannot find available visualizer for extension %s " % ext)
return avail_visus[0](filepath)
[docs]
@classmethod
def supported_extensions(cls) -> list:
"""List of file extensions supported by the visualizer."""
return [e for (e, args) in cls.EXTS]
[docs]
@classmethod
def from_name(cls, appname):
"""Return the visualizer class from the name of the application."""
for visu in cls.__subclasses__():
if visu.name == appname: return visu
raise cls.Error("`%s` is not among the list of supported visualizers" % appname)
[docs]
@classmethod
def all_visunames(cls):
"""List with the names of the visualizers supported."""
return sorted([visu.name for visu in cls.__subclasses__()])
####################
# Concrete classes #
####################
class Xcrysden(Visualizer):
name = "xcrysden"
EXTS = [
("xsf", "--xsf"),
("bxsf", "--bxsf"),
]
class V_Sim(Visualizer):
name = "v_sim"
EXTS = [
("xsf", "--xsf"),
]
class Vesta(Visualizer):
is_macosx_app = is_macosx()
name = "vesta"
EXTS = [
("xsf", ""),
]
class Ovito(Visualizer):
is_macosx_app = is_macosx()
name = "ovito"
EXTS = [
("POSCAR", ""),
]
class Avogadro(Visualizer):
is_macosx_app = is_macosx()
name = "avogadro"
EXTS = [
("cif", ""),
]