Skip to content

Commit

Permalink
Merge pull request #73 from mogproject/topic-terminal-restore-#71
Browse files Browse the repository at this point in the history
fix to restore terminal settings clearly closes #71
  • Loading branch information
mogproject committed Oct 21, 2015
2 parents 132cd12 + 60eede5 commit bde8bd3
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 67 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.1'
__version__ = '1.1.2'
12 changes: 10 additions & 2 deletions src/easy_menu/easy_menu.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import division, print_function, absolute_import, unicode_literals

import sys
import signal
from easy_menu.view import Terminal
from easy_menu.controller import CommandExecutor
from easy_menu.setting.setting import Setting
from easy_menu.logger import SystemLogger
from easy_menu.util import network_util
from easy_menu.util import network_util, term_util
from easy_menu.exceptions import EasyMenuError


Expand All @@ -14,10 +15,13 @@ def main(stdin=None, stdout=None, stderr=None):
Main function
"""

# for terminal restoration
signal.signal(signal.SIGTERM, term_util.restore_term_func(sys.stdin))

base_setting = Setting(stdin=stdin, stdout=stdout, stderr=stderr)

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

t = Terminal(
Expand All @@ -38,4 +42,8 @@ def main(stdin=None, stdout=None, stderr=None):
except EasyMenuError as e:
base_setting.stdout.write('%s: %s\n' % (e.__class__.__name__, e))
return 2
except IOError as e:
# maybe killed by outside
base_setting.stdout.write('\n%s: %s\n' % (e.__class__.__name__, e))
return 3
return 0
22 changes: 12 additions & 10 deletions src/easy_menu/setting/setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, config_path=None, work_dir=None, root_menu=None, encoding=Non
('config_path', config_path),
('work_dir', self._search_work_dir(work_dir, config_path, is_url)),
('root_menu', {} if root_menu is None else root_menu),
('encoding', self._find_encoding(encoding, stdout)),
('encoding', encoding),
('lang', self._find_lang(lang)),
('width', width),
('stdin', stdin or sys.stdin),
Expand All @@ -54,15 +54,6 @@ def _find_lang(lang):
lang = locale.getdefaultlocale()[0]
return lang

@staticmethod
def _find_encoding(encoding, output):
if not encoding:
if hasattr(output, 'encoding'):
encoding = output.encoding
if not encoding:
encoding = locale.getpreferredencoding()
return encoding

@staticmethod
def _is_url(path):
return path is not None and bool(URL_PATTERN.match(path))
Expand All @@ -75,6 +66,17 @@ def _search_work_dir(work_dir, config_path, is_url):
return os.path.dirname(config_path)
return work_dir

def resolve_encoding(self):
encoding = self.encoding
if not encoding:
if hasattr(self.stdout, 'encoding'):
encoding = self.stdout.encoding
if not encoding:
encoding = locale.getpreferredencoding()
if not encoding:
encoding = 'utf-8' # final fallback
return self.copy(encoding=encoding)

def parse_args(self, argv):
option, args = arg_parser.parser.parse_args(argv[1:])
path = None
Expand Down
13 changes: 13 additions & 0 deletions src/easy_menu/util/term_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ def _wrap_termios(_input, func):
return ret


def restore_term_func(_input=sys.stdin):
assert hasattr(_input, 'fileno'), 'Invalid input device.'

if os.name == 'nt':
return lambda signal, frame: None
fd = _input.fileno()
try:
original_settings = termios.tcgetattr(fd)
except termios.error:
return lambda signal, frame: None
return lambda signal, frame: termios.tcsetattr(fd, termios.TCSADRAIN, original_settings)


def getch(_input=sys.stdin):
"""Wait and get one character from input"""

Expand Down
121 changes: 67 additions & 54 deletions tests/easy_menu/setting/test_setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_init(self):
self.assertEqual(s5.work_dir, '/tmp')
self.assertEqual(s5.root_menu, {})

def test_find_encoding(self):
def test_resolve_encoding(self):
import io
import codecs

Expand All @@ -57,9 +57,8 @@ def test_find_encoding(self):
else:
out = io.TextIOWrapper(six.StringIO(), 'sjis')

self.assertEqual(Setting()._find_encoding(None, out), 'sjis')
self.assertEqual(Setting(stdout=out).encoding, 'sjis')
self.assertEqual(Setting(encoding='utf-8', stdout=out).encoding, 'utf-8')
self.assertEqual(Setting(stdout=out).resolve_encoding().encoding, 'sjis')
self.assertEqual(Setting(encoding='utf-8', stdout=out).resolve_encoding().encoding, 'utf-8')

def test_find_lang(self):
s = Setting()
Expand All @@ -68,7 +67,7 @@ def test_find_lang(self):
if old:
del os.environ['LANG']
self.assertEqual(s._find_lang('ja_JP'), 'ja_JP')
s._find_lang(None).islower() # return value depends on the system
s._find_lang(None) # return value depends on the system

os.environ['LANG'] = 'en_US'
self.assertEqual(s._find_lang(None), 'en_US')
Expand Down Expand Up @@ -155,28 +154,29 @@ def test_load_data(self):

with self.withAssertOutput('Reading file: tests/resources/minimum.yml\n', '') as (out, err):
self.assertEqual(
Setting(stdout=out, stderr=err)._load_data(False, 'tests/resources/minimum.yml'),
Setting(encoding='utf-8', stdout=out, stderr=err)._load_data(False, 'tests/resources/minimum.yml'),
{'': []}
)

with self.withAssertOutput('Reading file: tests/resources/nested.yml\n', '') as (out, err):
self.assertEqual(Setting(stdout=out, stderr=err)._load_data(False, 'tests/resources/nested.yml'),
{'Main Menu': [
{'Sub Menu 1': [
{'Menu 1': 'echo 1'},
{'Menu 2': 'echo 2'}
]},
{'Sub Menu 2': [
{'Sub Menu 3': [
{'Menu 3': 'echo 3'},
{'Menu 4': 'echo 4'}
]}, {'Menu 5': 'echo 5'}
]},
{'Menu 6': 'echo 6'}]}
)
self.assertEqual(
Setting(encoding='utf-8', stdout=out, stderr=err)._load_data(False, 'tests/resources/nested.yml'),
{'Main Menu': [
{'Sub Menu 1': [
{'Menu 1': 'echo 1'},
{'Menu 2': 'echo 2'}
]},
{'Sub Menu 2': [
{'Sub Menu 3': [
{'Menu 3': 'echo 3'},
{'Menu 4': 'echo 4'}
]}, {'Menu 5': 'echo 5'}
]},
{'Menu 6': 'echo 6'}]}
)
with self.withAssertOutput('Reading file: tests/resources/with_meta.yml\n', '') as (out, err):
self.assertEqual(
Setting(stdout=out, stderr=err)._load_data(False, 'tests/resources/with_meta.yml'),
Setting(encoding='utf-8', stdout=out, stderr=err)._load_data(False, 'tests/resources/with_meta.yml'),
{'meta': {'work_dir': '/tmp'},
'Main Menu': [{'Menu 1': 'echo 1'}, {'Menu 2': 'echo 2'}, {'Menu 3': 'echo 3'}, {'Menu 4': 'echo 4'},
{'Menu 5': 'echo 5'}, {'Menu 6': 'echo 6'}]}
Expand All @@ -190,9 +190,9 @@ def test_load_data(self):
)
with self.withAssertOutput('Reading file: %s\n' % self._testfile('minimum.yml'), '') as (out, err):
self.assertEqual(
Setting(work_dir=os.path.abspath('tests/resources'), stdout=out, stderr=err)._load_data(False,
'minimum.yml'),
{'': []}
Setting(
work_dir=os.path.abspath('tests/resources'), encoding='utf-8', stdout=out, stderr=err
)._load_data(False, 'minimum.yml'), {'': []}
)
# SJIS
with self.withAssertOutput('Reading file: tests/resources/sjis_ja.yml\n', '') as (out, err):
Expand All @@ -218,27 +218,31 @@ def test_load_data(self):
)
# multiple commands
with self.withAssertOutput('Reading file: tests/resources/multi_commands.yml\n', '') as (out, err):
self.assertEqual(Setting(stdout=out, stderr=err)._load_data(False, 'tests/resources/multi_commands.yml'),
{'Main Menu': [
{'Sub Menu 1': [
{'Menu 1': ['echo 1', 'echo 2']},
]},
{'Sub Menu 2': [
{'Sub Menu 3': [
{'Menu 3': 'echo 3'},
{'Menu 4': 'echo 4'}
]}, {'Menu 5': 'echo 5'}
]},
{'Menu 6': ['echo 1', 'echo 2', 'false', 'echo 3']}]}
)
self.assertEqual(Setting(encoding='utf-8', stdout=out, stderr=err)._load_data(
False, 'tests/resources/multi_commands.yml'),
{'Main Menu': [
{'Sub Menu 1': [
{'Menu 1': ['echo 1', 'echo 2']},
]},
{'Sub Menu 2': [
{'Sub Menu 3': [
{'Menu 3': 'echo 3'},
{'Menu 4': 'echo 4'}
]}, {'Menu 5': 'echo 5'}
]},
{'Menu 6': ['echo 1', 'echo 2', 'false', 'echo 3']}]}
)
# template
with self.withAssertOutput('Reading file: tests/resources/with_template.yml\n', '') as (out, err):
self.assertEqual(Setting(stdout=out, stderr=err)._load_data(False, 'tests/resources/with_template.yml'),
{'Main Menu': [
{'Menu 1': 'echo 1'},
{'Menu 2': 'echo 2'},
{'Menu 3': 'echo 3'},
]})
self.assertEqual(
Setting(
encoding='utf-8', stdout=out, stderr=err
)._load_data(False, 'tests/resources/with_template.yml'),
{'Main Menu': [
{'Menu 1': 'echo 1'},
{'Menu 2': 'echo 2'},
{'Menu 3': 'echo 3'},
]})
with self.withAssertOutput('Reading file: tests/resources/with_template_utf8_ja.yml\n', '') as (out, err):
self.assertEqual(
Setting(
Expand Down Expand Up @@ -267,10 +271,10 @@ def test_load_data(self):
def test_load_data_dynamic(self):
with self.withAssertOutput(
"""Executing: echo '{"Main Menu":[{"Menu 1":"echo 1"},{"Menu 2":"echo 2"}]}'\n""", '') as (out, err):
self.assertEqual(Setting(stdout=out, stderr=err)._load_data(
self.assertEqual(Setting(encoding='utf-8', stdout=out, stderr=err)._load_data(
True,
"""echo '{"Main Menu":[{"Menu 1":"echo 1"},{"Menu 2":"echo 2"}]}'"""),
{'Main Menu': [{'Menu 1': 'echo 1'}, {'Menu 2': 'echo 2'}]}
{'Main Menu': [{'Menu 1': 'echo 1'}, {'Menu 2': 'echo 2'}]}
)

@mock.patch('easy_menu.setting.setting.urlopen')
Expand All @@ -280,7 +284,7 @@ def test_load_data_http(self, urlopen_mock):

with self.withAssertOutput('Reading from URL: http://localhost/xxx.yml\n', '') as (out, err):
self.assertEqual(
Setting(stdout=out, stderr=err)._load_data(False, 'http://localhost/xxx.yml'),
Setting(encoding='utf-8', stdout=out, stderr=err)._load_data(False, 'http://localhost/xxx.yml'),
{'title': [{'a': 'x'}, {'b': 'y'}]}
)

Expand Down Expand Up @@ -380,7 +384,9 @@ def test_load_data_encoding_error_cmd(self):
def test_load_meta(self):
with self.withAssertOutput('Reading file: %s\n' % self._testfile('minimum.yml'), '') as (out, err):
self.assertEqual(
Setting(self._testfile('minimum.yml'), stdout=out, stderr=err).load_meta().work_dir,
Setting(
self._testfile('minimum.yml'), encoding='utf-8', stdout=out, stderr=err
).load_meta().work_dir,
os.path.join(os.path.abspath(os.path.curdir), 'tests', 'resources')
)
with self.withAssertOutput('', '') as (out, err):
Expand All @@ -400,33 +406,38 @@ def test_load_meta(self):
)
with self.withAssertOutput('Reading file: %s\n' % self._testfile('with_meta.yml'), '') as (out, err):
self.assertEqual(
Setting(self._testfile('with_meta.yml'), stdout=out, stderr=err).load_meta().work_dir,
Setting(
self._testfile('with_meta.yml'), encoding='utf-8', stdout=out, stderr=err
).load_meta().work_dir,
'/tmp'
)
with self.withAssertOutput('Reading file: %s\n' % self._testfile('with_meta.yml'), '') as (out, err):
self.assertEqual(
Setting(self._testfile('with_meta.yml'), work_dir='/var/tmp', stdout=out,
Setting(self._testfile('with_meta.yml'), work_dir='/var/tmp', encoding='utf-8', stdout=out,
stderr=err).load_meta().work_dir,
'/tmp'
)

def test_load_config(self):
with self.withAssertOutput('Reading file: %s\n' % self._testfile('minimum.yml'), '') as (out, err):
self.assertEqual(
Setting(config_path=self._testfile('minimum.yml'), stdout=out, stderr=err).load_config().root_menu,
Setting(
config_path=self._testfile('minimum.yml'), encoding='utf-8', stdout=out, stderr=err
).load_config().root_menu,
{'': []}
)
path = os.path.join(os.path.abspath(os.path.curdir), 'tests', 'resources', 'flat.yml')
with self.withAssertOutput('Reading file: %s\n' % path, '') as (out, err):
self.assertEqual(
Setting(config_path=self._testfile('flat.yml'), stdout=out, stderr=err).load_config().root_menu,
Setting(
config_path=self._testfile('flat.yml'), encoding='utf-8', stdout=out, stderr=err
).load_config().root_menu,
{
'Main Menu': [{'Menu 1': 'echo 1'}, {'Menu 2': 'echo 2'}, {'Menu 3': 'echo 3'},
{'Menu 4': 'echo 4'},
{'Menu 5': 'echo 5'}, {'Menu 6': 'echo 6'}]}
)


@unittest.skipUnless(os.name != 'nt', 'requires POSIX compatible')
def test_load_config_dynamic(self):
path = os.path.join(os.path.abspath(os.path.curdir), 'tests', 'resources', 'with_dynamic.yml')
Expand All @@ -436,7 +447,9 @@ def test_load_config_dynamic(self):
])
with self.withAssertOutput(expect, '') as (out, err):
self.assertEqual(
Setting(config_path=self._testfile('with_dynamic.yml'), stdout=out, stderr=err).load_config().root_menu,
Setting(
config_path=self._testfile('with_dynamic.yml'), encoding='utf-8', stdout=out, stderr=err
).load_config().root_menu,
{'Main Menu': [{'Menu 1': 'echo 1'}, {'Sub Menu': [{'Menu 2': 'echo 2'}, {'Menu 3': 'echo 3'}]}]}
)

Expand All @@ -461,5 +474,5 @@ def test_load_config_error(self):
self.assertRaisesRegexp(
ConfigError,
r'^%s: %s$' % (path.replace('\\', '\\\\'), expect),
lambda: Setting(config_path=path, stdout=out, stderr=err).load_config()
lambda: Setting(config_path=path, encoding='utf-8', stdout=out, stderr=err).load_config()
)

0 comments on commit bde8bd3

Please sign in to comment.