Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/planemo/cli.py @ 5:9b1c78e6ba9c draft default tip
"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
| author | shellac |
|---|---|
| date | Mon, 01 Jun 2020 08:59:25 -0400 |
| parents | 79f47841a781 |
| children |
comparison
equal
deleted
inserted
replaced
| 4:79f47841a781 | 5:9b1c78e6ba9c |
|---|---|
| 1 """The module describes a CLI framework extending ``click``.""" | |
| 2 import functools | |
| 3 import logging.config | |
| 4 import os | |
| 5 import shutil | |
| 6 import sys | |
| 7 import traceback | |
| 8 | |
| 9 import click | |
| 10 from six.moves.urllib.request import urlopen | |
| 11 | |
| 12 from planemo import __version__ | |
| 13 from planemo.exit_codes import ExitCodeException | |
| 14 from planemo.galaxy import profiles | |
| 15 from .config import ( | |
| 16 OptionSource, | |
| 17 read_global_config, | |
| 18 ) | |
| 19 from .io import error | |
| 20 | |
| 21 | |
| 22 CONTEXT_SETTINGS = dict(auto_envvar_prefix='PLANEMO') | |
| 23 COMMAND_ALIASES = { | |
| 24 "l": "lint", | |
| 25 "o": "open", | |
| 26 "t": "test", | |
| 27 "s": "serve", | |
| 28 } | |
| 29 | |
| 30 | |
| 31 class Context(object): | |
| 32 """Describe context of Planemo computation. | |
| 33 | |
| 34 Handles cross cutting concerns for Planemo such as verbose log | |
| 35 tracking, the definition of the Planemo workspace (``~/.planemo``), | |
| 36 and the global configuraton defined in ``~/.planemo.yml``. | |
| 37 """ | |
| 38 | |
| 39 def __init__(self): | |
| 40 """Construct a Context object using execution environment.""" | |
| 41 self.home = os.getcwd() | |
| 42 self._global_config = None | |
| 43 # Will be set by planemo CLI driver | |
| 44 self.verbose = False | |
| 45 self.planemo_config = None | |
| 46 self.planemo_directory = None | |
| 47 self.option_source = {} | |
| 48 | |
| 49 def set_option_source(self, param_name, option_source, force=False): | |
| 50 """Specify how an option was set.""" | |
| 51 if not force: | |
| 52 assert param_name not in self.option_source, "No option source for [%s]" % param_name | |
| 53 self.option_source[param_name] = option_source | |
| 54 | |
| 55 def get_option_source(self, param_name): | |
| 56 """Return OptionSource value indicating how the option was set.""" | |
| 57 assert param_name in self.option_source, "No option source for [%s]" % param_name | |
| 58 return self.option_source[param_name] | |
| 59 | |
| 60 @property | |
| 61 def global_config(self): | |
| 62 """Read Planemo's global configuration. | |
| 63 | |
| 64 As defined most simply by ~/.planemo.yml. | |
| 65 """ | |
| 66 if self._global_config is None: | |
| 67 self._global_config = read_global_config(self.planemo_config) | |
| 68 return self._global_config or {} | |
| 69 | |
| 70 def log(self, msg, *args): | |
| 71 """Log a message to stderr.""" | |
| 72 if args: | |
| 73 msg %= args | |
| 74 click.echo(msg, file=sys.stderr) | |
| 75 | |
| 76 def vlog(self, msg, *args, **kwds): | |
| 77 """Log a message to stderr only if verbose is enabled.""" | |
| 78 if self.verbose: | |
| 79 self.log(msg, *args) | |
| 80 if kwds.get("exception", False): | |
| 81 traceback.print_exc(file=sys.stderr) | |
| 82 | |
| 83 @property | |
| 84 def workspace(self): | |
| 85 """Create and return Planemo's workspace. | |
| 86 | |
| 87 By default this will be ``~/.planemo``. | |
| 88 """ | |
| 89 if not self.planemo_directory: | |
| 90 raise Exception("No planemo workspace defined.") | |
| 91 workspace = self.planemo_directory | |
| 92 return self._ensure_directory(workspace, "workspace") | |
| 93 | |
| 94 @property | |
| 95 def galaxy_profiles_directory(self): | |
| 96 """Create a return a directory for storing Galaxy profiles.""" | |
| 97 path = os.path.join(self.workspace, "profiles") | |
| 98 return self._ensure_directory(path, "Galaxy profiles") | |
| 99 | |
| 100 def _ensure_directory(self, path, name): | |
| 101 if not os.path.exists(path): | |
| 102 os.makedirs(path) | |
| 103 if not os.path.isdir(path): | |
| 104 template = "Planemo %s directory [%s] unavailable." | |
| 105 message = template % (name, path) | |
| 106 raise Exception(message) | |
| 107 return path | |
| 108 | |
| 109 def exit(self, exit_code): | |
| 110 """Exit planemo with the supplied exit code.""" | |
| 111 self.vlog("Exiting planemo with exit code [%d]" % exit_code) | |
| 112 raise ExitCodeException(exit_code) | |
| 113 | |
| 114 def cache_download(self, url, destination): | |
| 115 cache = os.path.join(self.workspace, "cache") | |
| 116 if not os.path.exists(cache): | |
| 117 os.makedirs(cache) | |
| 118 filename = os.path.basename(url) | |
| 119 cache_destination = os.path.join(cache, filename) | |
| 120 if not os.path.exists(cache_destination): | |
| 121 with urlopen(url) as fh: | |
| 122 content = fh.read() | |
| 123 if len(content) == 0: | |
| 124 raise Exception("Failed to download [%s]." % url) | |
| 125 with open(cache_destination, "wb") as f: | |
| 126 f.write(content) | |
| 127 | |
| 128 shutil.copy(cache_destination, destination) | |
| 129 | |
| 130 | |
| 131 pass_context = click.make_pass_decorator(Context, ensure=True) | |
| 132 cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), | |
| 133 'commands')) | |
| 134 | |
| 135 | |
| 136 def list_cmds(): | |
| 137 """List planemo commands from commands folder.""" | |
| 138 rv = [] | |
| 139 for filename in os.listdir(cmd_folder): | |
| 140 if filename.endswith('.py') and \ | |
| 141 filename.startswith('cmd_'): | |
| 142 rv.append(filename[len("cmd_"):-len(".py")]) | |
| 143 rv.sort() | |
| 144 return rv | |
| 145 | |
| 146 | |
| 147 def name_to_command(name): | |
| 148 """Convert a subcommand name to the cli function for that command. | |
| 149 | |
| 150 Command <X> is defined by the method 'planemo.commands.cmd_<x>:cli', | |
| 151 this method uses `__import__` to load and return that method. | |
| 152 """ | |
| 153 try: | |
| 154 if sys.version_info[0] == 2: | |
| 155 name = name.encode('ascii', 'replace') | |
| 156 mod_name = 'planemo.commands.cmd_' + name | |
| 157 mod = __import__(mod_name, None, None, ['cli']) | |
| 158 except ImportError as e: | |
| 159 error("Problem loading command %s, exception %s" % (name, e)) | |
| 160 return | |
| 161 return mod.cli | |
| 162 | |
| 163 | |
| 164 class PlanemoCLI(click.MultiCommand): | |
| 165 | |
| 166 def list_commands(self, ctx): | |
| 167 return list_cmds() | |
| 168 | |
| 169 def get_command(self, ctx, name): | |
| 170 if name in COMMAND_ALIASES: | |
| 171 name = COMMAND_ALIASES[name] | |
| 172 return name_to_command(name) | |
| 173 | |
| 174 | |
| 175 def command_function(f): | |
| 176 """Extension point for processing kwds after click callbacks.""" | |
| 177 @functools.wraps(f) | |
| 178 def handle_blended_options(*args, **kwds): | |
| 179 profile = kwds.get("profile", None) | |
| 180 if profile: | |
| 181 ctx = args[0] | |
| 182 profile_defaults = profiles.ensure_profile( | |
| 183 ctx, profile, **kwds | |
| 184 ) | |
| 185 _setup_profile_options(ctx, profile_defaults, kwds) | |
| 186 | |
| 187 _setup_galaxy_source_options(args[0], kwds) | |
| 188 | |
| 189 try: | |
| 190 return f(*args, **kwds) | |
| 191 except ExitCodeException as e: | |
| 192 sys.exit(e.exit_code) | |
| 193 | |
| 194 return pass_context(handle_blended_options) | |
| 195 | |
| 196 | |
| 197 EXCLUSIVE_OPTIONS_LIST = [ | |
| 198 ] | |
| 199 | |
| 200 | |
| 201 def _setup_galaxy_source_options(ctx, kwds): | |
| 202 for exclusive_options in EXCLUSIVE_OPTIONS_LIST: | |
| 203 option_source = {} | |
| 204 for option in exclusive_options: | |
| 205 if option in kwds: | |
| 206 option_source[option] = ctx.get_option_source(option) | |
| 207 else: | |
| 208 option_source[option] = None | |
| 209 | |
| 210 most_authoratative_source = None | |
| 211 most_authoratative_source_options = [] | |
| 212 for key, value in option_source.items(): | |
| 213 if value is None: | |
| 214 continue | |
| 215 if most_authoratative_source is None or value.value < most_authoratative_source.value: | |
| 216 most_authoratative_source = value | |
| 217 most_authoratative_source_options = [key] | |
| 218 elif value == most_authoratative_source: | |
| 219 most_authoratative_source_options.append(key) | |
| 220 | |
| 221 if most_authoratative_source != OptionSource.default and len(most_authoratative_source_options) > 1: | |
| 222 raise click.UsageError("Cannot specify multiple of %s" % most_authoratative_source_options) | |
| 223 | |
| 224 for option in exclusive_options: | |
| 225 if option in kwds and option not in most_authoratative_source_options: | |
| 226 del kwds[option] | |
| 227 | |
| 228 | |
| 229 def _setup_profile_options(ctx, profile_defaults, kwds): | |
| 230 for key, value in profile_defaults.items(): | |
| 231 option_present = key in kwds | |
| 232 option_cli_specified = option_present and (ctx.get_option_source(key) == OptionSource.cli) | |
| 233 use_profile_option = not option_present or not option_cli_specified | |
| 234 if use_profile_option: | |
| 235 kwds[key] = value | |
| 236 ctx.set_option_source( | |
| 237 key, OptionSource.profile, force=True | |
| 238 ) | |
| 239 | |
| 240 | |
| 241 @click.command(cls=PlanemoCLI, context_settings=CONTEXT_SETTINGS) | |
| 242 @click.version_option(__version__) | |
| 243 @click.option('-v', '--verbose', is_flag=True, | |
| 244 help='Enables verbose mode.') | |
| 245 @click.option('--config', | |
| 246 default="~/.planemo.yml", | |
| 247 envvar="PLANEMO_GLOBAL_CONFIG_PATH", | |
| 248 help="Planemo configuration YAML file.") | |
| 249 @click.option('--directory', | |
| 250 default="~/.planemo", | |
| 251 envvar="PLANEMO_GLOBAL_WORKSPACE", | |
| 252 help="Workspace for planemo.") | |
| 253 @pass_context | |
| 254 def planemo(ctx, config, directory, verbose, configure_logging=True): | |
| 255 """A command-line toolkit for building tools and workflows for Galaxy. | |
| 256 | |
| 257 Check out the full documentation for Planemo online | |
| 258 http://planemo.readthedocs.org or open with ``planemo docs``. | |
| 259 | |
| 260 All the individual planemo commands support the ``--help`` option, for | |
| 261 example use ``planemo lint --help`` for more details on checking tools. | |
| 262 """ | |
| 263 ctx.verbose = verbose | |
| 264 if configure_logging: | |
| 265 logging_config = { | |
| 266 'version': 1, | |
| 267 'disable_existing_loggers': False, | |
| 268 'formatters': { | |
| 269 'verbose': { | |
| 270 'format': '%(name)s %(levelname)s %(asctime)s: %(message)s' | |
| 271 }, | |
| 272 'simple': { | |
| 273 'format': '%(name)s %(levelname)s: %(message)s' | |
| 274 }, | |
| 275 }, | |
| 276 'handlers': { | |
| 277 'console': { | |
| 278 'level': 'DEBUG', | |
| 279 'class': 'logging.StreamHandler', | |
| 280 'formatter': 'simple' if not verbose else 'verbose' | |
| 281 }, | |
| 282 }, | |
| 283 'loggers': { | |
| 284 # Suppress CWL is beta warning, for Planemo purposes - it is absolutely not. | |
| 285 'galaxy.tools.parser.factory': { | |
| 286 'handlers': ['console'], | |
| 287 'propagate': False, | |
| 288 'level': 'ERROR' if not verbose else "DEBUG", | |
| 289 }, | |
| 290 'galaxy.tools.deps.commands': { | |
| 291 'handlers': ['console'], | |
| 292 'propagate': False, | |
| 293 'level': 'ERROR' if not verbose else "DEBUG", | |
| 294 }, | |
| 295 'galaxy': { | |
| 296 'handlers': ['console'], | |
| 297 'propagate': False, | |
| 298 'level': 'INFO' if not verbose else "DEBUG", | |
| 299 }, | |
| 300 # @jmchilton | |
| 301 # I'm fixing up Planemo's lint functionality for CWL and I keep seeing this for the | |
| 302 # schema metadata stuff (e.g. in the workflows repo). "rdflib.term WARNING: | |
| 303 # http://schema.org/docs/!DOCTYPE html does not look like a valid URI, trying to | |
| 304 # serialize this will break.". I'm going to suppress this warning I think, or are the | |
| 305 # examples wrong and should declare their namespaces differently in some way? | |
| 306 # @mr-c | |
| 307 # That particular warning is worth suppressing. A PR to silence it permanently would be very welcome! | |
| 308 # https://github.com/RDFLib/rdflib/blob/master/rdflib/term.py#L225 | |
| 309 'rdflib.term': { | |
| 310 'handlers': ['console'], | |
| 311 'propagate': False, | |
| 312 'level': 'ERROR' if not verbose else "DEBUG", | |
| 313 } | |
| 314 }, | |
| 315 'root': { | |
| 316 'handlers': ['console'], | |
| 317 'propagate': False, | |
| 318 'level': 'WARNING' if not verbose else "DEBUG", | |
| 319 } | |
| 320 } | |
| 321 logging.config.dictConfig(logging_config) | |
| 322 ctx.planemo_config = os.path.expanduser(config) | |
| 323 ctx.planemo_directory = os.path.expanduser(directory) | |
| 324 | |
| 325 | |
| 326 __all__ = ( | |
| 327 "command_function", | |
| 328 "Context", | |
| 329 "list_cmds", | |
| 330 "name_to_command", | |
| 331 "planemo", | |
| 332 ) |
