From 5dc37a7fbe7c5995433cbc2ddab8b89d531dde1e Mon Sep 17 00:00:00 2001 From: Erik Bernoth Date: Thu, 13 Jun 2013 17:12:05 +0200 Subject: [PATCH 1/5] serial_io: First Prototype Implements the idea described in GitHub Issue #60. The idea here is the following. The main function of our serial class on top of the already existing pyserial is a commandline like interface. We send a `cmd()` and receive it's output back. The corresponding method currently sends the command over the pyserial interface and reads from it what comes back. Everything until the end of the command is removed from the output and also the prompt after the command and everything that follows it. Then the filtered output is returned. If some step before takes longer then the timeout, then the function returns the incomplete parsing result. The last output and confidence about it's result are stored in the object for a later review by the user. Signed-off-by: Erik Bernoth --- monk_tf/serial_io.py | 144 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 monk_tf/serial_io.py diff --git a/monk_tf/serial_io.py b/monk_tf/serial_io.py new file mode 100644 index 0000000..b41d2a8 --- /dev/null +++ b/monk_tf/serial_io.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +# MONK automated test framework +# +# Copyright (C) 2013 DResearch Fahrzeugelektronik GmbH +# Written and maintained by MONK Developers +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version +# 3 of the License, or (at your option) any later version. +# + +import os +import time +import logging + +import serial + +class SerialConsole(serial.Serial): + """ console like interface on top of pyserial + + The job of this class is to allow a console like input-output behaviour + between the user of this class and a serial interface. To achieve this some + abstraction layers are necessary. + """ + + _DEFAULT_PROMPT=">>> " + + + def __init__(self, *args, **kwargs): + """ + :param prompt: set a prompt for the communication + """ + self._logger = logging.getLogger(__name__) + self.prompt = Serial._DEFAULT_PROMPT + if args: + self.prompt = prompt + super(Serial,self).__init__(*args, **kwargs) + + + def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): + """ send a command and retrieve it's response. + + Text that might be in the buffer before the command is ignored, as well + as text beginning from the prompt after the command. The command text + itself and the prompt afterwards are the borders of the command output, + that is returned. + + The basic structure of a command is like that:: + + + # optional (EOLs inside the command output are treated as any character) + + + We try to just grab the ```` and return it. + + :param msg: the shell command you want to execute over the serial line. + + :param prompt: the prompt that signals that a command is treated and + the next command can be sent. If None, the object's + default is used and if that is also not set the default + python prompt is used: ``>>> ``. + + :param sleep_length: defines how long the process should sleep until + the next iteration of the loop is started. + + :param timeout: defines in seconds how long this method should take at + most. + + :param linesep: the line separator used for the communication. It + defaults to ``\n`` + + :param return: the command output. + """ + class State: + LEFT_OVER=1 + CMD_OUTPUT=2 + # prepare + cmd = msg.strip() + linesep + self.write(cmd) + state = State.LEFT_OVER + out = "" + start_time = time.time() + while True: + if time.time() - start_time >= timeout: + self.__last_confidence = False + self.__last_cmd = msg + self.__last_output = out + return out + time.sleep(sleep_length) + out += self.read(self.inWaiting()) + if state == State.LEFT_OVER: + pos = out.find(cmd) + if pos >= 0: + # forget everything until including the command + out = out[pos+len(cmd):] + state = State.CMD_OUTPUT + elif state == State.CMD_OUTPUT: + pos = out.find(prompt) + if pos >= 0: + # strip the prompt and everything afterwards + out = out[:pos] + self.__last_confidence = True + self.__last_cmd = cmd + self.__last_output = out + return out + + + @property + def prompt(self): + try: + return self.__prompt + except AttributeError: + return None + + + @prompt.setter + def prompt(self,new_prompt): + self.__prompt = new_prompt + + + @property + def last_confidence(self): + try: + return self.__last_confidence + except AttributeError: + return None + + + @property + def last_cmd(self): + try: + return self.__last_cmd + except AttributeError: + return None + + + @property + def last_output(self): + try: + return self.__last_output + except AttributeError: + return None From d3ee0be1ee7f8e35abeffc0014a6318d9ae754cb Mon Sep 17 00:00:00 2001 From: Erik Bernoth Date: Mon, 17 Jun 2013 13:43:58 +0200 Subject: [PATCH 2/5] serial_io: separated read and write process Makes testing easier and the program itself more flexible. Also increases the likeness of this interface to serial_conn. See GitHub Issue #60. Signed-off-by: Erik Bernoth --- monk_tf/serial_io.py | 68 +++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/monk_tf/serial_io.py b/monk_tf/serial_io.py index b41d2a8..06f45ea 100644 --- a/monk_tf/serial_io.py +++ b/monk_tf/serial_io.py @@ -27,6 +27,9 @@ class SerialConsole(serial.Serial): _DEFAULT_PROMPT=">>> " + class ReadState: + LEFT_OVER=1 + FOUND_START=2 def __init__(self, *args, **kwargs): """ @@ -55,6 +58,9 @@ def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): We try to just grab the ```` and return it. + .. note:: The results from the last output can be optained with the + attributes ``last_cmd``, ``last_confidence`` and ``last_output``. + :param msg: the shell command you want to execute over the serial line. :param prompt: the prompt that signals that a command is treated and @@ -73,13 +79,45 @@ def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): :param return: the command output. """ - class State: - LEFT_OVER=1 - CMD_OUTPUT=2 # prepare cmd = msg.strip() + linesep self.write(cmd) - state = State.LEFT_OVER + self.__last_cmd = cmd + self.__last_confidence, self.__last_output = self.read_until( + cmd,prompt, sleep_length, timeout) + return self.last_output + + + def read_until(self, end_strip, start_strip=None, sleep_length=.1, timeout=5): + """ read until end strip found + + This function reads everything available in the buffer, then waits + ``sleep_length`` seconds and then starts again, either until it finds + ``end_strip`` in the buffered text or the ``timeout`` runs out. + Everything will be deleted, beginning from the ``end_strip``, because + that is expected to be knowledge the user already has. + + The same way as ``end_strip`` works on the end, you can also define a + ``start_strip``, which will delete everything in the buffer until the + start_strip is found. + + :param end_strip: The text which terminates the search. A string as + excepted by :py:func:`str.find`. + + :param start_strip: The text which starts the search. A string as + expected by :py:func:`str.find`. + + :param sleep_length: defines how long the process should sleep until + the next iteration of the loop is started. + + :param timeout: defines in seconds how long this method should take at + most. + + :return: (boolean, string) - The first param is True as long as the + timeout didn't deplete. The second param contains the output + as far as it was received. + """ + state = ReadState.LEFT_OVER if start_strip else ReadState.FOUND_START out = "" start_time = time.time() while True: @@ -90,21 +128,19 @@ class State: return out time.sleep(sleep_length) out += self.read(self.inWaiting()) - if state == State.LEFT_OVER: - pos = out.find(cmd) + if state == ReadState.LEFT_OVER: + pos = out.find(start_strip) if pos >= 0: - # forget everything until including the command - out = out[pos+len(cmd):] - state = State.CMD_OUTPUT - elif state == State.CMD_OUTPUT: - pos = out.find(prompt) + # forget everything up to including the start_strip + out = out[pos+len(start_strip):] + state = ReadState.FOUND_START + elif state == ReadState.FOUND_START: + pos = out.find(end_strip) if pos >= 0: - # strip the prompt and everything afterwards + # strip the end_strip and everything afterwards out = out[:pos] - self.__last_confidence = True - self.__last_cmd = cmd - self.__last_output = out - return out + return True, out + @property From ce2a966568eaf17ff5a6f865a05b9e1c83131472 Mon Sep 17 00:00:00 2001 From: Erik Bernoth Date: Tue, 18 Jun 2013 10:56:06 +0200 Subject: [PATCH 3/5] serial_io: fundamental unit tests and debugging Added tests for Object creation, a basic cmd() and 2 cases for the read_until methods. The last method needed an additonal unit test, because start_strip and end_strip should be tested separately. See GitHub Issue #60. Signed-off-by: Erik Bernoth --- monk_tf/serial_io.py | 69 ++++++++++++--------- test/test_serial_io.py | 136 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 test/test_serial_io.py diff --git a/monk_tf/serial_io.py b/monk_tf/serial_io.py index 06f45ea..5cb2659 100644 --- a/monk_tf/serial_io.py +++ b/monk_tf/serial_io.py @@ -17,7 +17,7 @@ import serial -class SerialConsole(serial.Serial): +class SerialIO(serial.Serial): """ console like interface on top of pyserial The job of this class is to allow a console like input-output behaviour @@ -36,13 +36,14 @@ def __init__(self, *args, **kwargs): :param prompt: set a prompt for the communication """ self._logger = logging.getLogger(__name__) - self.prompt = Serial._DEFAULT_PROMPT - if args: - self.prompt = prompt - super(Serial,self).__init__(*args, **kwargs) + self.prompt = SerialIO._DEFAULT_PROMPT + if "prompt" in args: + self.prompt = args["prompt"] + args.pop("prompt") + super(SerialIO,self).__init__(*args, **kwargs) - def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): + def cmd(self, msg, prompt=None, sleep_time=.1, timeout=5, linesep="\n"): """ send a command and retrieve it's response. Text that might be in the buffer before the command is ignored, as well @@ -53,7 +54,7 @@ def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): The basic structure of a command is like that:: - # optional (EOLs inside the command output are treated as any character) + # optional (EOLs inside the command output are treated as any character) We try to just grab the ```` and return it. @@ -68,7 +69,7 @@ def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): default is used and if that is also not set the default python prompt is used: ``>>> ``. - :param sleep_length: defines how long the process should sleep until + :param sleep_time: defines how long the process should sleep until the next iteration of the loop is started. :param timeout: defines in seconds how long this method should take at @@ -79,20 +80,25 @@ def cmd(self, msg, prompt=None, sleep_length=.1, timeout=5, linesep="\n"): :param return: the command output. """ - # prepare + prompt = prompt if prompt else self.prompt cmd = msg.strip() + linesep self.write(cmd) - self.__last_cmd = cmd - self.__last_confidence, self.__last_output = self.read_until( - cmd,prompt, sleep_length, timeout) + self._last_cmd = cmd + self._logger.debug(str((self, cmd, prompt, sleep_time, timeout))) + self._last_confidence, self._last_output = self.read_until( + prompt, + cmd, + sleep_time, + timeout + ) return self.last_output - def read_until(self, end_strip, start_strip=None, sleep_length=.1, timeout=5): + def read_until(self, end_strip, start_strip=None, sleep_time=.1, timeout=5): """ read until end strip found This function reads everything available in the buffer, then waits - ``sleep_length`` seconds and then starts again, either until it finds + ``sleep_time`` seconds and then starts again, either until it finds ``end_strip`` in the buffered text or the ``timeout`` runs out. Everything will be deleted, beginning from the ``end_strip``, because that is expected to be knowledge the user already has. @@ -101,13 +107,19 @@ def read_until(self, end_strip, start_strip=None, sleep_length=.1, timeout=5): ``start_strip``, which will delete everything in the buffer until the start_strip is found. + All of that is a generalization of how the method is used in cmd(). The + idea is that a command line execution over serial repeats the written + command, then writes the command output and finally prints a new + prompt. Therefore it makes sense to look for the command and the prompt + as the delimiters of the desired part of the output. + :param end_strip: The text which terminates the search. A string as excepted by :py:func:`str.find`. :param start_strip: The text which starts the search. A string as expected by :py:func:`str.find`. - :param sleep_length: defines how long the process should sleep until + :param sleep_time: defines how long the process should sleep until the next iteration of the loop is started. :param timeout: defines in seconds how long this method should take at @@ -117,24 +129,23 @@ def read_until(self, end_strip, start_strip=None, sleep_length=.1, timeout=5): timeout didn't deplete. The second param contains the output as far as it was received. """ - state = ReadState.LEFT_OVER if start_strip else ReadState.FOUND_START + state = SerialIO.ReadState.LEFT_OVER if start_strip else SerialIO.ReadState.FOUND_START out = "" start_time = time.time() while True: if time.time() - start_time >= timeout: - self.__last_confidence = False - self.__last_cmd = msg - self.__last_output = out - return out - time.sleep(sleep_length) + return False, out + # this should be also executed before first loop + # because this function is also used by write + time.sleep(sleep_time) out += self.read(self.inWaiting()) - if state == ReadState.LEFT_OVER: + if state == SerialIO.ReadState.LEFT_OVER: pos = out.find(start_strip) if pos >= 0: # forget everything up to including the start_strip out = out[pos+len(start_strip):] - state = ReadState.FOUND_START - elif state == ReadState.FOUND_START: + state = SerialIO.ReadState.FOUND_START + elif state == SerialIO.ReadState.FOUND_START: pos = out.find(end_strip) if pos >= 0: # strip the end_strip and everything afterwards @@ -146,20 +157,20 @@ def read_until(self, end_strip, start_strip=None, sleep_length=.1, timeout=5): @property def prompt(self): try: - return self.__prompt + return self._prompt except AttributeError: return None @prompt.setter def prompt(self,new_prompt): - self.__prompt = new_prompt + self._prompt = new_prompt @property def last_confidence(self): try: - return self.__last_confidence + return self._last_confidence except AttributeError: return None @@ -167,7 +178,7 @@ def last_confidence(self): @property def last_cmd(self): try: - return self.__last_cmd + return self._last_cmd except AttributeError: return None @@ -175,6 +186,6 @@ def last_cmd(self): @property def last_output(self): try: - return self.__last_output + return self._last_output except AttributeError: return None diff --git a/test/test_serial_io.py b/test/test_serial_io.py new file mode 100644 index 0000000..7f6535b --- /dev/null +++ b/test/test_serial_io.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# +# MONK Automated Testing Framework +# +# Copyright (C) 2012-2013 DResearch Fahrzeugelektronik GmbH, project-monk@dresearch-fe.de +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version +# 3 of the License, or (at you option) any later version. +# + +import logging + +from nose import tools as nt + +from monk_tf import serial_io as sio + + +def test_simple(): + """serial_io: check wether creating a SerialIO object works + """ + #nothing to prepare + # + #execute + sut = sio.SerialIO() + #assert + nt.ok_(sut, "should contain a monk_tf.serial_io.SerialIO object, but instead contains this: '{}'".format(sut)) + + +def test_cmd_set_attribs(): + """serial_io: check wether cmd automatically updates the attributes + """ + # prepare + send_cmd = "qwer" + expected_calls = ['write', 'read_until'] + expected_cmd = send_cmd + "\n" + expected_confidence = True + expected_output = 'abcd' + sut = MockSerialIOCmd((True, expected_output)) + # execute + sut.cmd(send_cmd) + # evaluate + nt.eq_(expected_calls, sut.calls) + nt.eq_(expected_cmd, sut.last_cmd) + nt.eq_(expected_confidence, sut.last_confidence) + nt.eq_(expected_output, sut.last_output) + # clean up + # not needed + + +def test_read_until_strips_end(): + """serial_io: check wether reading really strips end_strip + """ + # prepare + expected_calls = ["read"] + expected_out = "trewq\n" + expected_confidence = True + in_strip = "abcd" + in_sleep = 0.0 + in_mock_output = expected_out + in_strip + sut = MockSerialIORead(in_mock_output) + # execute + out_confidence, output = sut.read_until(in_strip, sleep_time=in_sleep) + # evalute + nt.eq_(expected_calls, sut.calls) + nt.eq_(expected_confidence, out_confidence) + nt.eq_(expected_out, output) + # clean up + # not needed + + +def test_read_until_strips_start(): + """serial_io: check wether reading really strips start_strip + """ + # prepare + expected_calls = ["read", "read"] + expected_out = "trewq\n" + expected_confidence = True + in_start_strip = "abcd\n" + in_stop_strip = "dcba" + in_sleep = 0.0 + in_mock_output = in_start_strip + expected_out + in_stop_strip + sut = MockSerialIORead(in_mock_output) + # execute + out_confidence, output = sut.read_until( + in_stop_strip, + start_strip=in_start_strip, + sleep_time=in_sleep + ) + # evalute + nt.eq_(expected_calls, sut.calls) + nt.eq_(expected_confidence, out_confidence) + nt.eq_(expected_out, output) + # clean up + # not needed + + +class MockSerialIOCmd(sio. SerialIO): + """ mocks specifically for testing the cmd() method + """ + + def __init__(self, readout=None): + self.calls = [] + self.readout = readout + super(MockSerialIOCmd, self).__init__() + + + def write(self,cmd=None): + self.calls.append("write") + + + def read_until(self, cmd=None, prompt=None, sleep_time=None, timeout=None, + start_strip=None): + self.calls.append("read_until") + return self.readout + + +class MockSerialIORead(sio.SerialIO): + + def __init__(self, readout=None): + self.calls = [] + self.readout = readout + super(MockSerialIORead, self).__init__() + + + def write(self,cmd=None): + self.calls.append("write") + + + def read(self, number=None): + self.calls.append("read") + return self.readout + + def inWaiting(self): + return 0 From 51516b1d6a64ec930cefb612fc947b90cd91cfee Mon Sep 17 00:00:00 2001 From: Erik Bernoth Date: Tue, 18 Jun 2013 15:47:28 +0200 Subject: [PATCH 4/5] serial_io: Add confidence output to cmd() the command should force the user to react to outputs that don't meet the prompt. See GitHub Issue #60. Signed-off-by: Erik Bernoth --- monk_tf/serial_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monk_tf/serial_io.py b/monk_tf/serial_io.py index 5cb2659..56bc92e 100644 --- a/monk_tf/serial_io.py +++ b/monk_tf/serial_io.py @@ -91,7 +91,7 @@ def cmd(self, msg, prompt=None, sleep_time=.1, timeout=5, linesep="\n"): sleep_time, timeout ) - return self.last_output + return self.last_confidence, self.last_output def read_until(self, end_strip, start_strip=None, sleep_time=.1, timeout=5): From 2883271d564400e018ceea5295150c96acc7a3b2 Mon Sep 17 00:00:00 2001 From: Erik Bernoth Date: Tue, 9 Jul 2013 17:01:53 +0200 Subject: [PATCH 5/5] serial_io.py: simplify cmd() method According to the research in Issue #60 (https://github.com/DFE/MONK/issues/60), the simplest implementation for best case scenarios is only 2 lines. This commit implements those 2 lines instead of the complex behaviour before. Issue #69 (https://github.com/DFE/MONK/issues/69) shall improve or replace that method with a more practical implementation, if that is not already finished with Issue #61 (https://github.com/DFE/MONK/issues/61). See Github Issue #60 for details. Signed-off-by: Erik Bernoth Acked-by: Steffen Sledz Acked-by: Eik Binschek --- monk_tf/serial_io.py | 170 ++++++------------------------------------- 1 file changed, 22 insertions(+), 148 deletions(-) diff --git a/monk_tf/serial_io.py b/monk_tf/serial_io.py index 56bc92e..cc5c3d1 100644 --- a/monk_tf/serial_io.py +++ b/monk_tf/serial_io.py @@ -25,167 +25,41 @@ class SerialIO(serial.Serial): abstraction layers are necessary. """ - _DEFAULT_PROMPT=">>> " - - class ReadState: - LEFT_OVER=1 - FOUND_START=2 def __init__(self, *args, **kwargs): """ :param prompt: set a prompt for the communication """ self._logger = logging.getLogger(__name__) - self.prompt = SerialIO._DEFAULT_PROMPT - if "prompt" in args: - self.prompt = args["prompt"] - args.pop("prompt") + if "linesep" in args: + self.linesep = args["linesep"] + args.pop("linesep") super(SerialIO,self).__init__(*args, **kwargs) - def cmd(self, msg, prompt=None, sleep_time=.1, timeout=5, linesep="\n"): - """ send a command and retrieve it's response. - - Text that might be in the buffer before the command is ignored, as well - as text beginning from the prompt after the command. The command text - itself and the prompt afterwards are the borders of the command output, - that is returned. - - The basic structure of a command is like that:: - - - # optional (EOLs inside the command output are treated as any character) - - - We try to just grab the ```` and return it. - - .. note:: The results from the last output can be optained with the - attributes ``last_cmd``, ``last_confidence`` and ``last_output``. - - :param msg: the shell command you want to execute over the serial line. - - :param prompt: the prompt that signals that a command is treated and - the next command can be sent. If None, the object's - default is used and if that is also not set the default - python prompt is used: ``>>> ``. - - :param sleep_time: defines how long the process should sleep until - the next iteration of the loop is started. - - :param timeout: defines in seconds how long this method should take at - most. - - :param linesep: the line separator used for the communication. It - defaults to ``\n`` - - :param return: the command output. - """ - prompt = prompt if prompt else self.prompt - cmd = msg.strip() + linesep - self.write(cmd) - self._last_cmd = cmd - self._logger.debug(str((self, cmd, prompt, sleep_time, timeout))) - self._last_confidence, self._last_output = self.read_until( - prompt, - cmd, - sleep_time, - timeout - ) - return self.last_confidence, self.last_output - - - def read_until(self, end_strip, start_strip=None, sleep_time=.1, timeout=5): - """ read until end strip found - - This function reads everything available in the buffer, then waits - ``sleep_time`` seconds and then starts again, either until it finds - ``end_strip`` in the buffered text or the ``timeout`` runs out. - Everything will be deleted, beginning from the ``end_strip``, because - that is expected to be knowledge the user already has. - - The same way as ``end_strip`` works on the end, you can also define a - ``start_strip``, which will delete everything in the buffer until the - start_strip is found. - - All of that is a generalization of how the method is used in cmd(). The - idea is that a command line execution over serial repeats the written - command, then writes the command output and finally prints a new - prompt. Therefore it makes sense to look for the command and the prompt - as the delimiters of the desired part of the output. - - :param end_strip: The text which terminates the search. A string as - excepted by :py:func:`str.find`. - - :param start_strip: The text which starts the search. A string as - expected by :py:func:`str.find`. - - :param sleep_time: defines how long the process should sleep until - the next iteration of the loop is started. + def cmd(self, msg): + """ send a command and receive it's output - :param timeout: defines in seconds how long this method should take at - most. - - :return: (boolean, string) - The first param is True as long as the - timeout didn't deplete. The second param contains the output - as far as it was received. + :param msg: the command that shall be sent, with or without line + separator in the end + :return: the output of the command as created by target device """ - state = SerialIO.ReadState.LEFT_OVER if start_strip else SerialIO.ReadState.FOUND_START - out = "" - start_time = time.time() - while True: - if time.time() - start_time >= timeout: - return False, out - # this should be also executed before first loop - # because this function is also used by write - time.sleep(sleep_time) - out += self.read(self.inWaiting()) - if state == SerialIO.ReadState.LEFT_OVER: - pos = out.find(start_strip) - if pos >= 0: - # forget everything up to including the start_strip - out = out[pos+len(start_strip):] - state = SerialIO.ReadState.FOUND_START - elif state == SerialIO.ReadState.FOUND_START: - pos = out.find(end_strip) - if pos >= 0: - # strip the end_strip and everything afterwards - out = out[:pos] - return True, out - - - - @property - def prompt(self): - try: - return self._prompt - except AttributeError: - return None - - - @prompt.setter - def prompt(self,new_prompt): - self._prompt = new_prompt - + # a command will only be executed, if it ends in a linebreak + self.write(msg.strip() + self.linesep) + # remove first line (the cmd itself), last line (the next prompt) + # and unnecesary \r characters from the output + return self.linesep.join(self.readall().replace("\r","").split("\n")[1:-1]) @property - def last_confidence(self): - try: - return self._last_confidence - except AttributeError: - return None - - - @property - def last_cmd(self): + def linesep(self): + """ In most cases ``os.linesep``. + """ try: - return self._last_cmd + return self._linesep except AttributeError: - return None - + self.linesep = os.linesep + return self._linesep - @property - def last_output(self): - try: - return self._last_output - except AttributeError: - return None + @linesep.setter + def linesep(self, lsep): + self._linesep = lsep