Skip to content

Commit

Permalink
Merge pull request #86 from mogproject/topic-cwd-env-#74
Browse files Browse the repository at this point in the history
implement command cwd/env configuration closes #74
  • Loading branch information
mogproject committed Oct 31, 2015
2 parents 702bfec + 79ba53f commit 63249bf
Show file tree
Hide file tree
Showing 22 changed files with 1,052 additions and 605 deletions.
2 changes: 1 addition & 1 deletion src/easy_menu/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.1.5'
__version__ = '1.2.0'
39 changes: 28 additions & 11 deletions src/easy_menu/controller/command_executor.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
from __future__ import division, print_function, absolute_import, unicode_literals

from mog_commons.command import execute_command
from easy_menu.entity.command import Command
from easy_menu.logger.logger import Logger


class CommandExecutor(object):
def __init__(self, work_dir=None, logger=None):
self.work_dir = work_dir
def __init__(self, logger, encoding, stdin, stdout, stderr):
assert isinstance(logger, Logger)

self.logger = logger
self.encoding = encoding
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr

def execute(self, command):
assert isinstance(command, Command)

ret_code = 0
for command_line in command.command_lines:
try:
self.logger.info('Command started: %s' % command_line.cmd)
ret_code = execute_command(command_line.cmd, shell=True,
cwd=command_line.meta.work_dir, env=command_line.meta.env,
stdin=self.stdin, stdout=self.stdout, stderr=self.stderr,
cmd_encoding=self.encoding)
self.logger.info('Command ended with return code: %d' % ret_code)

def execute(self, cmd, stdin, stdout, stderr, encoding):
self.logger.info('Command started: %s' % cmd)
try:
ret_code = execute_command(cmd, shell=True, cwd=self.work_dir,
stdin=stdin, stdout=stdout, stderr=stderr, cmd_encoding=encoding)
self.logger.info('Command ended with return code: %d' % ret_code)
except KeyboardInterrupt:
self.logger.info('Command interrupted.')
ret_code = 130
except KeyboardInterrupt:
self.logger.info('Command interrupted.')
ret_code = 130

# if a command fails, the successors will not run
if ret_code != 0:
break
return ret_code
8 changes: 6 additions & 2 deletions src/easy_menu/easy_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ def main(stdin=None, stdout=None, stderr=None):
base_setting = Setting(stdin=stdin, stdout=stdout, stderr=stderr)

try:
setting = base_setting.parse_args(sys.argv).resolve_encoding().lookup_config().load_meta().load_config()
executor = CommandExecutor(setting.work_dir, SystemLogger(setting.encoding))
setting = base_setting.parse_args(sys.argv).resolve_encoding().lookup_config().load_config()
executor = CommandExecutor(SystemLogger(setting.encoding), setting.encoding, stdin, stdout, stderr)

t = Terminal(
setting.root_menu,
Expand All @@ -47,6 +47,10 @@ def main(stdin=None, stdout=None, stderr=None):
# maybe killed by outside
base_setting.stdout.write('\n%s: %s\n' % (e.__class__.__name__, e))
return 3
except OSError as e:
# e.g. working directory does not exist
base_setting.stdout.write('%s: %s\n' % (e.__class__.__name__, e))
return 4
finally:
# assume to restore original terminal settings
restore_term_func(None, None)
Expand Down
9 changes: 9 additions & 0 deletions src/easy_menu/entity/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
KEYWORD_META = 'meta'
KEYWORD_INCLUDE = 'include'
KEYWORD_EVAL = 'eval'

from .item import Item
from .meta import Meta
from .command_line import CommandLine
from .command import Command
from .menu import Menu
50 changes: 50 additions & 0 deletions src/easy_menu/entity/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import division, print_function, absolute_import, unicode_literals

import six
from mog_commons.case_class import CaseClass
from mog_commons.string import to_unicode, is_unicode
from mog_commons.collection import get_single_item
from easy_menu.entity.command_line import CommandLine


class Command(CaseClass):
def __init__(self, title, command_lines):
"""
:param title:
:param command_lines:
:return:
"""
assert is_unicode(title)
assert isinstance(command_lines, list) and all(isinstance(x, CommandLine) for x in command_lines)

CaseClass.__init__(self, ('title', title), ('command_lines', command_lines))

@staticmethod
def parse(data, meta, encoding='utf-8'):
"""
Parse one command operation.
:param data: dict:
:param meta: Meta: meta configuration inherited from the parent menu
:param encoding: string:
:return: Command:
"""
from easy_menu.entity import Meta

assert isinstance(data, dict), 'Command must be dict, not %s.' % type(data).__name__
assert isinstance(meta, Meta)

if len(data) != 1:
raise ValueError('Command should have only one element, not %s.' % len(data))

title, content = get_single_item(data)
assert isinstance(title, six.string_types), 'Command title must be string, not %s' % type(title).__name__
title = to_unicode(title, encoding)

if isinstance(content, six.string_types):
# single command
return Command(title, [CommandLine.parse(content, meta, encoding)])
elif isinstance(content, list):
# command list
return Command(title, [CommandLine.parse(d, meta, encoding) for d in content])
else:
raise ValueError('Invalid command content type: %s' % type(content).__name__)
44 changes: 44 additions & 0 deletions src/easy_menu/entity/command_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import division, print_function, absolute_import, unicode_literals

import six
from mog_commons.case_class import CaseClass
from mog_commons.collection import get_single_item
from mog_commons.string import to_unicode, is_unicode, is_strlike
from easy_menu.entity import Meta


class CommandLine(CaseClass):
def __init__(self, cmd, meta):
"""
:param cmd:
:param meta:
:return:
"""
assert is_unicode(cmd)
assert isinstance(meta, Meta)

CaseClass.__init__(self, ('cmd', cmd), ('meta', meta))

@staticmethod
def parse(data, meta, encoding='utf-8'):
"""
Parse one command line.
:param data: string or dict:
:param meta: Meta: meta configuration inherited from the parent menu
:param encoding: string:
:return: CommandLine:
"""
assert isinstance(meta, Meta)

def f(s):
return to_unicode(s, encoding)

if is_strlike(data):
return CommandLine(f(data), meta)
elif isinstance(data, dict):
cmd, params = get_single_item(data)
assert is_strlike(cmd), 'cmd must be string, not %s.' % type(cmd).__name__
new_meta = meta.updated(params, encoding)
return CommandLine(to_unicode(cmd, encoding), new_meta)
else:
raise ValueError('CommandLine must be string or dict, not %s.' % type(data).__name__)
78 changes: 78 additions & 0 deletions src/easy_menu/entity/item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import division, print_function, absolute_import, unicode_literals

from abc import ABCMeta
import six
from mog_commons.case_class import CaseClass
from mog_commons.collection import get_single_item


@six.add_metaclass(ABCMeta)
class Item(CaseClass):
"""
Abstract item class
"""

@staticmethod
def _is_command_like(content):
"""
We assume the data is command when it is a string or one-element dict which contains string key and dict value
:param content: object to parse
:return: true when the data is command-like
"""
if isinstance(content, six.string_types):
return True

if isinstance(content, list):
for item in content:
ok = False
if isinstance(item, six.string_types):
ok = True
if isinstance(item, dict) and len(item) == 1:
cmd, attr = get_single_item(item)
ok = isinstance(cmd, six.string_types) and isinstance(attr, dict)
if not ok:
return False
return True

return False

@staticmethod
def parse(data, meta, loader, encoding='utf-8', depth=0):
"""
:param data:
:param meta:
:param loader:
:param encoding:
:param depth: indicator for the nesting level
:return:
"""
from easy_menu.entity import Menu, Meta, Command, KEYWORD_META, KEYWORD_INCLUDE, KEYWORD_EVAL
from easy_menu.setting.loader import Loader

assert isinstance(data, dict), 'Item must be dict, not %s.' % type(data).__name__
assert isinstance(meta, Meta)
assert isinstance(loader, Loader)

# avoid for inclusion loops and stack overflow
assert depth < 50, 'Nesting level too deep.'

# if the data has meta key, it should be a menu.
if KEYWORD_META in data:
return Menu.parse(data, meta, loader, encoding, depth)

assert len(data) == 1, 'Item should have only one element, not %s.' % len(data)

title, content = get_single_item(data)

if title == KEYWORD_INCLUDE:
assert isinstance(content, six.string_types), \
'"include" section must have string content, not %s.' % type(content).__name__
return Menu.parse(loader.load(False, content), meta, loader, encoding, depth)
elif title == KEYWORD_EVAL:
assert isinstance(content, six.string_types), \
'"eval" section must have string content, not %s.' % type(content).__name__
return Menu.parse(loader.load(True, content), meta, loader, encoding, depth)
elif Item._is_command_like(content):
return Command.parse(data, meta, encoding)
else:
return Menu.parse(data, meta, loader, encoding, depth)
60 changes: 60 additions & 0 deletions src/easy_menu/entity/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import division, print_function, absolute_import, unicode_literals

import six
from mog_commons.case_class import CaseClass
from mog_commons.string import to_unicode, is_unicode
from mog_commons.collection import get_single_item
from easy_menu.entity import Command, Meta


class Menu(CaseClass):
"""
Menu is built from an one-element dict having title string as key and item list element as value
"""

def __init__(self, title, items, meta):
"""
:param title:
:param items:
:param meta:
:return:
"""
assert is_unicode(title)
assert isinstance(items, list) and all(isinstance(x, Menu) or isinstance(x, Command) for x in items)
assert isinstance(meta, Meta)

CaseClass.__init__(self,
('title', title),
('items', items),
('meta', meta))

@staticmethod
def parse(data, meta, loader, encoding='utf-8', depth=0):
"""
:param data:
:param meta:
:param loader:
:param encoding:
:param depth:
:return:
"""
from easy_menu.entity import KEYWORD_META, Item

assert isinstance(data, dict), 'Menu must be dict, not %s.' % type(data).__name__
assert isinstance(meta, Meta)

# read meta configurations
if KEYWORD_META in data:
meta = meta.updated(data[KEYWORD_META], encoding)
del data[KEYWORD_META]

assert len(data) == 1, 'Menu should have only one item, not %s.' % len(data)

title, content = get_single_item(data)
assert isinstance(title, six.string_types), 'Menu title must be string, not %s.' % type(title).__name__
assert isinstance(content, list), 'Menu content must be list, not %s.' % type(content).__name__
title = to_unicode(title, encoding)

items = [Item.parse(item, meta, loader, encoding, depth + 1) for item in content]
return Menu(title, items, meta)
67 changes: 67 additions & 0 deletions src/easy_menu/entity/meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import division, print_function, absolute_import, unicode_literals

import copy
import six
from mog_commons.case_class import CaseClass
from mog_commons.string import to_unicode, is_unicode, is_strlike


class Meta(CaseClass):
"""
Meta settings for running commands
"""

def __init__(self, work_dir=None, env=None):
"""
:param work_dir:
:param env:
:return:
"""
env = env or {}
assert work_dir is None or is_unicode(work_dir)
assert isinstance(env, dict) and all(is_unicode(k) and is_unicode(v) for k, v in env.items())

CaseClass.__init__(self, ('work_dir', work_dir), ('env', env))

def updated(self, data, encoding):
"""
Load configuration and return updated instance.
:param data: dict:
:return: Meta:
"""
assert isinstance(data, dict), 'Meta must be dist, not %s.' % type(data).__name__

functions = {
'work_dir': Meta._load_work_dir,
'env': Meta._load_env
}

ret = self.copy()
for k, v in data.items():
ret = functions.get(k, self.__unknown_field(k))(ret, v, encoding)
return ret

def _load_work_dir(self, data, encoding):
"""Overwrite working directory"""
assert is_strlike(data), 'work_dir must be string, not %s.' % type(data).__name__

self.work_dir = to_unicode(data, encoding)
return self

def _load_env(self, data, encoding):
"""Merge environment variables"""
assert isinstance(data, dict), 'env must be dict, not %s.' % type(data).__name__
assert all(is_strlike(k) and is_strlike(v) for k, v in data.items()), \
'env must be a dict of string key and string value.'

d = copy.copy(self.env)
d.update([(to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data.items()])
self.env = d
return self

@staticmethod
def __unknown_field(key):
def f(x, y, z):
raise ValueError('Unknown field: %s' % key)

return f

0 comments on commit 63249bf

Please sign in to comment.