-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #86 from mogproject/topic-cwd-env-#74
implement command cwd/env configuration closes #74
- Loading branch information
Showing
22 changed files
with
1,052 additions
and
605 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = '1.1.5' | ||
__version__ = '1.2.0' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.