├── .coveragerc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── docs ├── index.md ├── license.md ├── releases.md ├── tutorial │ └── tutorial.md └── user-guide │ ├── getting-started.md │ ├── installation.md │ ├── integrations.md │ └── testing.md ├── mkdocs.yml ├── setup.py ├── src └── python_shell │ ├── __init__.py │ ├── command │ ├── __init__.py │ ├── command.py │ └── interfaces.py │ ├── exceptions │ ├── __init__.py │ ├── base.py │ ├── process.py │ └── shell.py │ ├── shell │ ├── __init__.py │ ├── core.py │ ├── processing │ │ ├── __init__.py │ │ ├── interfaces.py │ │ └── process.py │ └── terminal │ │ ├── __init__.py │ │ ├── base.py │ │ ├── bash.py │ │ └── interfaces.py │ ├── util │ ├── __init__.py │ ├── streaming.py │ ├── terminal.py │ └── version.py │ └── version.py ├── tests ├── __init__.py ├── fixtures │ ├── __init__.py │ └── interfaces.py ├── test_command.py ├── test_exceptions.py ├── test_interfaces.py ├── test_process.py ├── test_shell.py ├── test_terminal_integrations.py └── test_util.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Alex Sokolov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | [run] 24 | omit = 25 | .tox/** 26 | tests/** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tox 2 | .coverage 3 | *.pyc 4 | *.pyo 5 | *.swp 6 | .venv* 7 | .env* 8 | *egg-info 9 | htmlcov 10 | .idea 11 | dist 12 | build 13 | site 14 | 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ### 2020-03-06 4 | 5 | * Added support for Python 3.9 and PyPy 6 | * Added proper documentation (based on mkdocs) 7 | * Added property _is_undefined_ for processes 8 | * Added `__version__` for the root package 9 | * Refactoring in exception classes 10 | * Added usage of **flake8** in Tox configurations 11 | 12 | ### 2020-03-20 13 | 14 | * Added support for non-blocking commands 15 | * Added option "wait" for running command 16 | * stdout and stderr of a command become stream 17 | * Improved test coverage 18 | 19 | ### 2020-02-29 20 | 21 | * Enabled support for list shell commands by dir(Shell) 22 | * Enabled command autocomplete in **BPython** and **IPython** for Shell 23 | * Added ability to run shell commands with no-identifier-like name 24 | * Added access to the last executed command even if exception was raised 25 | * Added property "errors" for stderr output 26 | * Added human-understandable behaviour for str() and repr() of Command instance 27 | * Some internal refactoring and bugfixes 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alex Sokolov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include VERSION 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS REPOSITORY IS NO LONGER MAINTAINED 2 | New repository is here: https://github.com/ATCode-space/python-shell 3 | 4 | 5 | # Python Shell Wrapper Library 6 | 7 | A flexible, easy-to-use library to integrate your Python script with Unix ecosystems. 8 | 9 | ## Why yet another one? 10 | 11 | This library comes with a few major points to use it: 12 | 13 | * It is easy and intuitive (see examples) 14 | * It's compatible with Python 2 (can be useful in old large systems) 15 | * Continuous support of the library 16 | 17 | ## Getting started 18 | 19 | This library is pretty easy to use: 20 | 21 | ```python 22 | from python_shell import Shell 23 | from python_shell.util.streaming import decode_stream 24 | 25 | Shell.ls('-l', '$HOME') # Equals "ls -l $HOME" 26 | 27 | command = Shell.whoami() # Equals "whoami" 28 | print(command) # Prints representation of command in shell 29 | 30 | print(command.command) # prints "whoami" 31 | print(repr(command)) # Does the same as above 32 | 33 | print(command.return_code) # prints "0" 34 | print(command.arguments) # prints "" 35 | 36 | print(decode_stream(command.output)) # Prints out command's stdout 37 | print(decode_stream(command.errors)) # Prints out command's stderr 38 | ``` 39 | 40 | To run any Bash command, you need to do it like this: 41 | ``` 42 | Shell.() 43 | ``` 44 | 45 | For example, you want to create a new folder: 46 | ```python 47 | Shell.mkdir('-p', '/tmp/new_folder') 48 | ``` 49 | 50 | It's also possible to run a command which name is not a valid Python identifier. 51 | To do this, use Shell class as a callable instance: 52 | ```python 53 | command = Shell('2to3') 54 | ``` 55 | 56 | When the command fails (returncode is non-zero), Shell throws a ShellException error. 57 | However, even if you didn't save a reference to your command, you still can access it. 58 | To do this, try 59 | ```python 60 | last_cmd = Shell.last_command 61 | ``` 62 | ### Installing 63 | 64 | Simply run 65 | 66 | ``` 67 | pip install python-shell 68 | ``` 69 | 70 | ## Integration with development tools 71 | 72 | **Shell** class now allows to list all available commands simply by 73 | ```python 74 | dir(Shell) 75 | ``` 76 | 77 | This feature enables autocomplete of commands in a few popular interfaces: 78 | - BPython 79 | - IPython 80 | 81 | ## Extending the basic functionality 82 | 83 | It's possible to extend the existing functionality without forking the project. 84 | The library provides an interface to add a custom Command class. 85 | 86 | ## Running the tests 87 | 88 | This library contains tests written using *unittest* module, so just run in the project directory 89 | 90 | ``` 91 | python -m unittest 92 | ``` 93 | 94 | Also it's possible to run tests using Tox: 95 | 96 | ```bash 97 | tox -e 98 | ``` 99 | 100 | Supported environments: 101 | 102 | - py27 103 | - py35 104 | - py36 105 | - py37 106 | - py38 107 | - coverage (using Python 3) 108 | - coverage (using Python 2.7) 109 | - pep8 (style checking) 110 | 111 | Other old versions of Python (e.g. 2.6, 3.4, etc) will never be supported. However, you always can implement such support in your forks. 112 | 113 | Test coverage is one of the top priority for this library: 114 | - Coverage using Python 2.7: 98% 115 | - Coverage using Python 3.x: 96% 116 | 117 | ## Documentation 118 | 119 | Official documentation is available [here](https://python-shell.readthedocs.io/). 120 | 121 | ## Authors 122 | 123 | * **Alex Sokolov** - *Author* - [Albartash](https://github.com/AlBartash) 124 | 125 | ## Contacts 126 | 127 | * Telegram channel with updates: [@bart_tools](http://t.me/bart_tools) 128 | 129 | ## License 130 | 131 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 132 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.4 -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Python-Shell library's documentation! 2 | 3 | ## Table of Contents 4 | 5 | * User Guide 6 | 7 | * [Getting Started](user-guide/getting-started.md) 8 | 9 | * [Installation](user-guide/installation.md) 10 | 11 | * [Integrations](user-guide/integrations.md) 12 | 13 | * [Testing](user-guide/testing.md) 14 | 15 | ## 16 | * About the Project 17 | 18 | * [License](license.md) 19 | 20 | * [Releases](releases.md) 21 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | **Copyright (c) 2020 Alex Sokolov** 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/releases.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | ## Release 1.0.4 4 | 5 | Date: 2020-03-06 6 | 7 | Changes: 8 | 9 | * Added support for Python 3.9 and PyPy 10 | * Added proper documentation (based on mkdocs) 11 | * Added property _is_undefined_ for processes 12 | * Added `__version__` for the root package 13 | * Refactoring in exception classes 14 | * Added usage of **flake8** in Tox configurations 15 | 16 | ## Release 1.0.3 17 | 18 | Date: 2020-03-20 19 | 20 | Changes: 21 | 22 | * Added support for non-blocking commands 23 | * Added option "wait" for running command 24 | * stdout and stderr of a command become stream 25 | * Improved test coverage 26 | 27 | 28 | ## Release 1.0.2 29 | Date: no date 30 | 31 | Changes: 32 | * Due to technical accident, release 1.0.2 was skipped. 33 | 34 | ## Release 1.0.1 35 | 36 | Date: 2020-02-29 37 | 38 | Changes: 39 | 40 | * Enabled support for list shell commands by dir(Shell) 41 | * Enabled command autocomplete in **BPython** and **IPython** for Shell 42 | * Added ability to run shell commands with no-identifier-like name 43 | * Added access to the last executed command even if exception was raised 44 | * Added property "errors" for stderr output 45 | * Added human-understandable behaviour for str() and repr() of Command instance 46 | * Some internal refactoring and bugfixes 47 | -------------------------------------------------------------------------------- /docs/tutorial/tutorial.md: -------------------------------------------------------------------------------- 1 | This space is in development. Will be published in the next release. 2 | -------------------------------------------------------------------------------- /docs/user-guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | This library is pretty easy to use: 4 | 5 | ```python 6 | from python_shell import Shell 7 | from python_shell.util.streaming import decode_stream 8 | 9 | Shell.ls('-l', '$HOME') # Equals "ls -l $HOME" 10 | 11 | command = Shell.whoami() # Equals "whoami" 12 | print(command) # Prints representation of command in shell 13 | 14 | print(command.command) # prints "whoami" 15 | print(repr(command)) # Does the same as above 16 | 17 | print(command.return_code) # prints "0" 18 | print(command.arguments) # prints "" 19 | 20 | print(decode_stream(command.output)) # Prints out command's stdout 21 | print(decode_stream(command.errors)) # Prints out command's stderr 22 | ``` 23 | 24 | To run any Bash command, you need to do it like this: 25 | ``` 26 | Shell.() 27 | ``` 28 | 29 | For example, you want to create a new folder: 30 | ```python 31 | Shell.mkdir('-p', '/tmp/new_folder') 32 | ``` 33 | 34 | It's also possible to run a command which name is not a valid Python identifier. 35 | To do this, use Shell class as a callable instance: 36 | ```python 37 | command = Shell('2to3') 38 | ``` 39 | 40 | When the command fails (returncode is non-zero), Shell throws a ShellException error. 41 | However, even if you didn't save a reference to your command, you still can access it. 42 | To do this, try 43 | ```python 44 | last_cmd = Shell.last_command 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/user-guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The python-shell library is developed as "pythonic" as possible, so 4 | does not require any additional non-Python dependencies. 5 | 6 | To install it, you need **pip** installed in the system. 7 | 8 | Then simply run 9 | ``` 10 | pip install python-shell 11 | ``` 12 | 13 | ## Supported Python versions 14 | 15 | This library supports the next Python versions: 16 | 17 | 18 | |   Version   |   Supported   | 19 | |:-------:|:---------:| 20 | | **3.9** | **Yes** | 21 | | **3.8** | **Yes** | 22 | | **3.7** | **Yes** | 23 | | **3.6** | **Yes** | 24 | | **3.5** | **Yes** | 25 | | 3.4 | No | 26 | | 3.3 | No | 27 | | 3.2 | No | 28 | | 3.1 | No | 29 | | 3.0 | No | 30 | | **2.7** | **Yes** | 31 | | <=2.6 | No | 32 | | **PyPy 2** | **Yes** | 33 | | **PyPy 3** | **Yes** | 34 | 35 |
36 | 37 | Despite the "Year 2020" for Python 2, this library will work with that. 38 | There're still plenty of stuff written with Python 2 and those "mammoths" 39 | won't evolute fast. 40 | 41 | Support for coming new versions is obvious, but there will be no 42 | additional compatibility with old versions listed in the table. 43 | There're few simple points for that: 44 | 1. Some huge and old projects with tons of legacy code written in Python 2 45 | should be surely working on 2.7. If not, then it's a good chance 46 | to do so - I see no particular reason for keeping 2.6 or older. 47 | 1. Projects which use Python 3 should use at least 3.5. Still, I see 48 | no reason for keeping older versions, as they do not have lots of 49 | useful things and are dangerous in general. 50 | 51 | ## Dependencies 52 | 53 | The **python-shell** library is designed to be using as less 54 | third-party dependencies as possible (for easier integration 55 | and more stability). However, support of Python 2.7 requires to 56 | keep such one - a [six](https://pypi.org/project/six/) module. -------------------------------------------------------------------------------- /docs/user-guide/integrations.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | 3 | Being a result of Python magic around "duck typing", **python-shell** 4 | tries to integrate with different tools for easier usage. 5 | One of the most popular features is autocompletion, which is somehow 6 | implemented for Shell commands to work with **Shell** class. 7 | However, not all the popular software "agreed" with that. 8 | 9 | ## Integrations with custom Python interpreters 10 | 11 | For now, autocompletion of **Shell** class is confirmed in a few 12 | popular custom Python interfaces (interpreters): 13 | 14 | * [BPython](https://github.com/bpython/bpython) 15 | 16 | * [IPython](https://ipython.org/) 17 | 18 | 19 | ## Integrations with IDEs 20 | 21 | Modern IDEs are complicated, as they provide a lot of functionality. 22 | Some of them, like PyCharm, use static analysis for it ([proof](https://youtrack.jetbrains.com/issue/PY-40943)). 23 | That's the reason why **Shell** autocompletion does not work in this IDE. 24 | -------------------------------------------------------------------------------- /docs/user-guide/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Running tests 4 | 5 | This library contains tests written using *unittest* module, so just run in the project directory 6 | 7 | ``` 8 | python -m unittest 9 | ``` 10 | 11 | Also it's possible to run tests using Tox: 12 | 13 | ```bash 14 | tox -e 15 | ``` 16 | 17 | Supported environments: 18 | 19 | - py27 20 | - py35 21 | - py36 22 | - py37 23 | - py38 24 | - py39 25 | - pypy (based on Python 2.7) 26 | - pypy3 (based on Python 3.6) 27 | 28 | ## Test coverage 29 | 30 | Test coverage is one of the top priority for this library. 31 | For the latest release: 32 | 33 | - Coverage using Python 2.7: 98% 34 | - Coverage using Python 3.x: 96% 35 | 36 | Tox environments: 37 | 38 | - coverage (using Python 3) 39 | - coverage (using Python 2.7) 40 | 41 | ## Code style checking 42 | 43 | There're a few more Tox environments for checking code style: 44 | 45 | - pep8 (style checking) 46 | - pylint (using Python 3) 47 | - pylint27 (using Python 2.7) 48 | 49 | For PEP8 check, the **pycodestyle** and **flake8** are used sequentially. 50 | Passing this check is required for merging the pull request. 51 | 52 | Pylint, in other hand, is added for additional check and is not used in release 53 | process. 54 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Python-Shell 2 | site_author: Alex Sokolov 3 | 4 | nav: 5 | - Home: index.md 6 | - User Guide: 7 | - Quickstart: user-guide/getting-started.md 8 | - Installation: user-guide/installation.md 9 | - Testing: user-guide/testing.md 10 | - Integrations: user-guide/integrations.md 11 | - Tutorial: tutorial/tutorial.md 12 | - About: 13 | - Releases: releases.md 14 | - License: license.md 15 | 16 | repo_url: https://github.com/bart-tools/python-shell 17 | 18 | theme: 19 | name: readthedocs 20 | 21 | authors: 22 | - Alex Sokolov 23 | 24 | copyright: Copyright © 2020 Alex Sokolov. 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from setuptools import find_packages 26 | from setuptools import setup 27 | import os 28 | import sys 29 | 30 | 31 | if sys.version_info[0] == 2: 32 | from io import open 33 | 34 | 35 | here = os.path.abspath(os.path.dirname(__file__)) 36 | 37 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 38 | long_description = f.read().strip() 39 | 40 | 41 | with open(os.path.join(here, 'VERSION'), encoding='utf-8') as f: 42 | version = f.read() 43 | 44 | 45 | setup( 46 | name='python-shell', 47 | version=version, 48 | description='Python Shell Wrapper library', 49 | long_description=long_description, 50 | long_description_content_type='text/markdown', 51 | url='https://github.com/bart-tools/python-shell', 52 | author='Alex Sokolov', 53 | author_email='volokos.alex@gmail.com', 54 | classifiers=[ 55 | 'Development Status :: 5 - Production/Stable', 56 | 'Intended Audience :: Developers', 57 | 'Topic :: Software Development', 58 | 'Topic :: Software Development :: Build Tools', 59 | 'Topic :: Software Development :: Libraries', 60 | 'Topic :: Software Development :: Libraries :: Python Modules', 61 | 'Topic :: System :: Shells', 62 | 'Topic :: System :: Systems Administration', 63 | 'License :: OSI Approved :: MIT License', 64 | 'Programming Language :: Python', 65 | 'Programming Language :: Python :: 2', 66 | 'Programming Language :: Python :: 2.7', 67 | 'Programming Language :: Python :: 3', 68 | 'Programming Language :: Python :: 3.5', 69 | 'Programming Language :: Python :: 3.6', 70 | 'Programming Language :: Python :: 3.7', 71 | 'Programming Language :: Python :: 3.8', 72 | 'Programming Language :: Python :: 3.9', 73 | 'Programming Language :: Python :: Implementation :: CPython', 74 | 'Programming Language :: Python :: Implementation :: PyPy', 75 | ], 76 | keywords='python shell bash', 77 | packages=find_packages(where='src', exclude=['tests']), 78 | package_dir={'': 'src'}, 79 | python_requires='>2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4', 80 | install_requires=['six>=1.14.0'], 81 | project_urls={ 82 | 'Source': 'https://github.com/bart-tools/python-shell', 83 | }, 84 | ) 85 | -------------------------------------------------------------------------------- /src/python_shell/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .shell import Shell 26 | from .version import get_version 27 | 28 | 29 | __all__ = ('Shell',) 30 | 31 | __version__ = get_version() 32 | -------------------------------------------------------------------------------- /src/python_shell/command/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .command import * 26 | from .interfaces import * 27 | 28 | 29 | __all__ = ( 30 | 'Command', 31 | 'ICommand' 32 | ) 33 | -------------------------------------------------------------------------------- /src/python_shell/command/command.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from python_shell.exceptions import CommandDoesNotExist 26 | from python_shell.exceptions import ShellException 27 | from python_shell.command.interfaces import ICommand 28 | from python_shell.util import AsyncProcess 29 | from python_shell.util import SyncProcess 30 | from python_shell.util import Subprocess 31 | 32 | 33 | __all__ = ('Command',) 34 | 35 | 36 | class Command(ICommand): 37 | """Simple decorator for shell commands""" 38 | 39 | _process = None 40 | _command = None 41 | 42 | def _validate_command(self, command_name): 43 | try: 44 | SyncProcess( 45 | "which", 46 | command_name, 47 | check=True, 48 | stdout=Subprocess.DEVNULL 49 | ).execute() 50 | except Subprocess.CalledProcessError: 51 | raise CommandDoesNotExist(self) 52 | 53 | def __init__(self, command_name): 54 | self._command = command_name 55 | 56 | def __call__(self, *args, **kwargs): 57 | """Executes the command with passed arguments 58 | and returns a Command instance""" 59 | 60 | self._validate_command(self._command) 61 | 62 | wait = kwargs.pop('wait', True) 63 | 64 | process_cls = SyncProcess if wait else AsyncProcess 65 | 66 | self._arguments = args 67 | 68 | self._process = process_cls( 69 | self._command, 70 | *args, 71 | **kwargs 72 | ) 73 | 74 | try: 75 | self._process.execute() 76 | except Subprocess.CalledProcessError: 77 | raise ShellException(self) 78 | 79 | return self 80 | 81 | @property 82 | def command(self): 83 | """Returns a string with the command""" 84 | return self._command 85 | 86 | @property 87 | def arguments(self): 88 | """Returns a string with the arguments passed to the command""" 89 | return ' '.join(self._arguments) 90 | 91 | @property 92 | def return_code(self): 93 | """Returns an integer code returned by the invoked command""" 94 | return self._process.returncode 95 | 96 | @property 97 | def output(self): 98 | """Returns an iterable object with output of the command""" 99 | return self._process.stdout 100 | 101 | @property 102 | def errors(self): 103 | """Returns an iterable object with output of the command 104 | from stderr 105 | """ 106 | return self._process.stderr 107 | 108 | def __str__(self): 109 | """Returns command's execution string""" 110 | return repr(self) 111 | 112 | def __repr__(self): 113 | """Returns command's execution string""" 114 | return ' '.join((self.command, self.arguments)) 115 | -------------------------------------------------------------------------------- /src/python_shell/command/interfaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import abc 26 | 27 | from six import with_metaclass 28 | 29 | __all__ = ('ICommand',) 30 | 31 | 32 | class ICommand(with_metaclass(abc.ABCMeta)): 33 | """Interface for shell Commands""" 34 | 35 | @property 36 | @abc.abstractmethod 37 | def command(self): 38 | """Returns a string with the command""" 39 | raise NotImplementedError 40 | 41 | @property 42 | @abc.abstractmethod 43 | def arguments(self): 44 | """Returns a string with the arguments passed to the command""" 45 | raise NotImplementedError 46 | 47 | @property 48 | @abc.abstractmethod 49 | def return_code(self): 50 | """Returns an integer code returned by the invoked command""" 51 | raise NotImplementedError 52 | 53 | @property 54 | @abc.abstractmethod 55 | def output(self): 56 | """Returns a string output of the invoked command from stdout""" 57 | raise NotImplementedError 58 | 59 | @property 60 | @abc.abstractmethod 61 | def errors(self): 62 | """Returns a string output of the invoked command from stderr""" 63 | raise NotImplementedError 64 | -------------------------------------------------------------------------------- /src/python_shell/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .process import * 26 | from .shell import * 27 | 28 | 29 | __all__ = ( 30 | 'CommandDoesNotExist', 31 | 'RunProcessError', 32 | 'ShellException', 33 | 'UndefinedProcess' 34 | ) 35 | -------------------------------------------------------------------------------- /src/python_shell/exceptions/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | __all__ = ('BaseShellException',) 26 | 27 | 28 | class BaseShellException(Exception): 29 | """Base exception for all exceptions within the library""" 30 | -------------------------------------------------------------------------------- /src/python_shell/exceptions/process.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .base import BaseShellException 26 | 27 | 28 | __all__ = ('RunProcessError', 'UndefinedProcess') 29 | 30 | 31 | class ProcessException(BaseShellException): 32 | """General exception class for Process failures""" 33 | 34 | 35 | class UndefinedProcess(ProcessException): 36 | """Raises when there's a try to use undefined process""" 37 | 38 | def __str__(self): 39 | return "Undefined process cannot be used" 40 | 41 | 42 | class RunProcessError(Exception): 43 | """Raised when process fails to be run""" 44 | 45 | def __init__(self, 46 | cmd, 47 | process_args=None, 48 | process_kwargs=None): 49 | 50 | self._cmd = cmd 51 | self._args = process_args 52 | self._kwargs = process_kwargs 53 | 54 | def __str__(self): 55 | return "Fail to run '{cmd} {args}'".format( 56 | cmd=self._cmd, 57 | args=' '.join(self._args) if self._args else '', 58 | ) 59 | -------------------------------------------------------------------------------- /src/python_shell/exceptions/shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .base import BaseShellException 26 | 27 | 28 | __all__ = ('CommandDoesNotExist', 'ShellException') 29 | 30 | 31 | class ShellException(BaseShellException): 32 | """Defines any exception caused by commands run in Shell""" 33 | 34 | _command = None 35 | 36 | def __init__(self, command): 37 | super(ShellException, self).__init__() 38 | self._command = command 39 | 40 | def __str__(self): 41 | return 'Shell command "{} {}" failed with return code {}'.format( 42 | self._command.command, 43 | self._command.arguments, 44 | self._command.return_code) 45 | 46 | 47 | class CommandDoesNotExist(ShellException): 48 | """Defines an exception when command does not exist in the environment""" 49 | def __init__(self, command): 50 | super(CommandDoesNotExist, self).__init__(command) 51 | 52 | def __str__(self): 53 | return 'Command "{}" does not exist'.format(self._command.command) 54 | -------------------------------------------------------------------------------- /src/python_shell/shell/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .core import Shell 26 | 27 | __all__ = ('Shell',) 28 | -------------------------------------------------------------------------------- /src/python_shell/shell/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from six import with_metaclass 26 | 27 | from python_shell.command import Command 28 | from python_shell.shell.terminal import TERMINAL_INTEGRATION_MAP 29 | from python_shell.util.terminal import get_current_terminal_name 30 | 31 | 32 | __all__ = ('Shell',) 33 | 34 | 35 | class MetaShell(type): 36 | 37 | __own_fields__ = ('last_command',) 38 | 39 | def __getattr__(cls, item): 40 | """Returns either own field or shell command object""" 41 | 42 | # NOTE(albartash): The next check ensures we wouldn't have troubles 43 | # in getting own fields even if forgetting to define 44 | # a related property for that. 45 | 46 | if item in cls.__own_fields__: 47 | return cls.__dict__[item] 48 | 49 | cls._last_command = Command(item) 50 | return cls._last_command 51 | 52 | def __dir__(cls): 53 | """Return list of available shell commands + own fields""" 54 | name = get_current_terminal_name() 55 | commands = TERMINAL_INTEGRATION_MAP[name]().available_commands 56 | return sorted( 57 | list(cls.__own_fields__) + commands 58 | ) 59 | 60 | @property 61 | def last_command(cls): 62 | """Returns last executed command""" 63 | return cls._last_command 64 | 65 | 66 | class Shell(with_metaclass(MetaShell)): 67 | """Simple decorator for Terminal using Subprocess""" 68 | 69 | _last_command = None 70 | 71 | def __new__(cls, command_name): 72 | """Returns an ICommand instance for specified command_name. 73 | This is useful for shell commands which names are not valid 74 | in Python terms as identifier. 75 | 76 | NOTE: This is not a constructor, as it could seem to be. 77 | """ 78 | return getattr(cls, command_name) 79 | -------------------------------------------------------------------------------- /src/python_shell/shell/processing/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .process import AsyncProcess 26 | from .process import SyncProcess 27 | 28 | 29 | __all__ = ( 30 | 'AsyncProcess', 31 | 'SyncProcess' 32 | ) 33 | -------------------------------------------------------------------------------- /src/python_shell/shell/processing/interfaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import abc 26 | from six import with_metaclass 27 | 28 | 29 | class IProcess(with_metaclass(abc.ABCMeta)): 30 | """Interface for defining Process wrappers""" 31 | 32 | @property 33 | @abc.abstractmethod 34 | def stderr(self): 35 | """Returns stderr output of process""" 36 | raise NotImplementedError 37 | 38 | @property 39 | @abc.abstractmethod 40 | def stdout(self): 41 | """Returns stdout output of process""" 42 | raise NotImplementedError 43 | 44 | @property 45 | @abc.abstractmethod 46 | def returncode(self): 47 | """Returns returncode of process""" 48 | raise NotImplementedError 49 | 50 | @property 51 | @abc.abstractmethod 52 | def is_finished(self): 53 | """Returns whether process has been completed""" 54 | raise NotImplementedError 55 | 56 | @abc.abstractmethod 57 | def execute(self): 58 | """Run the process instance""" 59 | raise NotImplementedError 60 | 61 | @property 62 | @abc.abstractmethod 63 | def is_undefined(self): 64 | """Returns whether process is undefined""" 65 | raise NotImplementedError 66 | -------------------------------------------------------------------------------- /src/python_shell/shell/processing/process.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import abc 26 | import os 27 | import subprocess 28 | 29 | from six import with_metaclass 30 | 31 | from python_shell.exceptions import RunProcessError 32 | from python_shell.exceptions import UndefinedProcess 33 | from python_shell.shell.processing.interfaces import IProcess 34 | from python_shell.util.version import is_python2_running 35 | 36 | 37 | __all__ = ('Subprocess', 'Process', 'SyncProcess', 'AsyncProcess') 38 | 39 | 40 | _PIPE = subprocess.PIPE 41 | 42 | if is_python2_running(): 43 | class _CalledProcessError(OSError): 44 | """A wrapper for Python 2 exceptions. 45 | 46 | Code is taken from CalledProcessError of Python 3 and adopted. 47 | """ 48 | 49 | def __init__(self, returncode, cmd, output=None, stderr=None): 50 | super(_CalledProcessError, self).__init__() 51 | 52 | self.returncode = returncode 53 | self.cmd = cmd 54 | self.stdout = output 55 | self.stderr = stderr 56 | 57 | def __str__(self): 58 | return "Command '%s' returned non-zero exit status %d." % ( 59 | self.cmd, self.returncode) 60 | 61 | else: 62 | _CalledProcessError = subprocess.CalledProcessError 63 | 64 | 65 | class StreamIterator(object): 66 | """A wrapper for retrieving data from subprocess streams""" 67 | 68 | def __init__(self, stream=None): 69 | """Initialize object with passed stream. 70 | 71 | If stream is None, that means process is undefined, 72 | and iterator will just raise StopIteration. 73 | """ 74 | 75 | self._stream = stream 76 | 77 | def __iter__(self): 78 | return self 79 | 80 | def __next__(self): 81 | """Returns next available line from passed stream""" 82 | 83 | if not self._stream: 84 | raise StopIteration 85 | 86 | line = self._stream.readline() 87 | if line: 88 | return line 89 | raise StopIteration 90 | 91 | next = __next__ 92 | 93 | 94 | class Process(IProcess): 95 | """A wrapper for process 96 | 97 | When process is not initialized (passed process=None to constructor), 98 | it is generally undefined, so neither completed nor running, 99 | but for practical reason, assume it has never been started. 100 | """ 101 | 102 | _process = None # process instance 103 | _args = None 104 | _kwargs = None 105 | 106 | PROCESS_IS_TERMINATED_CODE = -15 107 | 108 | def __init__(self, command, *args, **kwargs): 109 | self._command = command 110 | self._args = args 111 | self._kwargs = kwargs 112 | 113 | @property 114 | def stderr(self): 115 | """Returns stderr output of process""" 116 | 117 | return StreamIterator( 118 | stream=self._process and self._process.stderr or None 119 | ) 120 | 121 | @property 122 | def stdout(self): 123 | """Returns stdout output of process""" 124 | 125 | return StreamIterator( 126 | stream=self._process and self._process.stdout or None 127 | ) 128 | 129 | @property 130 | def returncode(self): # -> Union[int, None] 131 | """Returns returncode of process 132 | 133 | For undefined process, it returns None 134 | """ 135 | 136 | if self._process: 137 | self._process.poll() # Ensure we can get the returncode 138 | return self._process.returncode 139 | return None 140 | 141 | @property 142 | def is_finished(self): # -> Union[bool, None] 143 | """Returns whether process has been completed 144 | 145 | For undefined process, it returns None. 146 | """ 147 | if self._process: 148 | return self._process.returncode is not None 149 | return None 150 | 151 | @property 152 | def is_terminated(self): # -> Union[bool, None] 153 | """Returns whether process has been terminated 154 | 155 | For undefined process, it returns None. 156 | """ 157 | 158 | if self._process: 159 | return self.returncode == self.PROCESS_IS_TERMINATED_CODE 160 | return None 161 | 162 | @property 163 | def is_undefined(self): 164 | """Returns whether process is undefined""" 165 | return self._process is None 166 | 167 | def _make_command_execution_list(self, args): 168 | """Builds and returns a Shell command""" 169 | 170 | return [self._command] + list(map(str, args)) 171 | 172 | def terminate(self): 173 | """Terminates process if it's defined""" 174 | 175 | if self._process: 176 | self._process.terminate() 177 | 178 | # NOTE(albartash): It's needed, otherwise termination can happen 179 | # slower than next call of poll(). 180 | self._process.wait() 181 | else: 182 | raise UndefinedProcess 183 | 184 | def wait(self): 185 | """Wait until process is completed""" 186 | 187 | if self._process: 188 | self._process.wait() 189 | else: 190 | raise UndefinedProcess 191 | 192 | @abc.abstractmethod 193 | def execute(self): 194 | """Abstract method, to be implemented in derived classes""" 195 | 196 | raise NotImplementedError 197 | 198 | 199 | class SyncProcess(Process): 200 | """Process subclass for running process 201 | with waiting for its completion""" 202 | 203 | def execute(self): 204 | """Run a process in synchronous way""" 205 | 206 | arguments = self._make_command_execution_list(self._args) 207 | 208 | kwargs = { 209 | 'stdout': self._kwargs.get('stdout', Subprocess.PIPE), 210 | 'stderr': self._kwargs.get('stderr', Subprocess.PIPE), 211 | 'stdin': self._kwargs.get('stdin', Subprocess.PIPE) 212 | } 213 | 214 | try: 215 | self._process = subprocess.Popen( 216 | arguments, 217 | **kwargs 218 | ) 219 | except (OSError, ValueError): 220 | raise RunProcessError( 221 | cmd=arguments[0], 222 | process_args=arguments[1:], 223 | process_kwargs=kwargs 224 | ) 225 | 226 | if is_python2_running(): # Timeout is not supported in Python 2 227 | self._process.wait() 228 | else: 229 | self._process.wait(timeout=self._kwargs.get('timeout', None)) 230 | 231 | if self._process.returncode and self._kwargs.get('check', True): 232 | raise Subprocess.CalledProcessError( 233 | returncode=self._process.returncode, 234 | cmd=str(arguments) 235 | ) 236 | 237 | 238 | class AsyncProcess(Process): 239 | """Process subclass for running process 240 | with waiting for its completion""" 241 | 242 | def execute(self): 243 | """Run a process in asynchronous way""" 244 | 245 | arguments = self._make_command_execution_list(self._args) 246 | 247 | kwargs = { 248 | 'stdout': self._kwargs.get('stdout', Subprocess.PIPE), 249 | 'stderr': self._kwargs.get('stderr', Subprocess.PIPE), 250 | 'stdin': self._kwargs.get('stdin', Subprocess.PIPE) 251 | } 252 | 253 | try: 254 | self._process = subprocess.Popen( 255 | arguments, 256 | **kwargs 257 | ) 258 | except (OSError, ValueError): 259 | raise RunProcessError( 260 | cmd=arguments[0], 261 | process_args=arguments[1:], 262 | process_kwargs=kwargs 263 | ) 264 | 265 | 266 | class _SubprocessMeta(type): 267 | """Meta class for Subprocess""" 268 | 269 | _devnull = None 270 | 271 | @property 272 | def DEVNULL(cls): # -> int 273 | """Returns a DEVNULL constant compatible with all Pytho versions""" 274 | 275 | if is_python2_running(): 276 | if cls._devnull is None: 277 | cls._devnull = os.open(os.devnull, os.O_RDWR) 278 | else: 279 | cls._devnull = subprocess.DEVNULL 280 | 281 | return cls._devnull 282 | 283 | 284 | class Subprocess(with_metaclass(_SubprocessMeta, object)): 285 | """A wrapper for subprocess module""" 286 | 287 | CalledProcessError = _CalledProcessError 288 | PIPE = _PIPE 289 | -------------------------------------------------------------------------------- /src/python_shell/shell/terminal/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .bash import * 26 | 27 | 28 | TERMINAL_INTEGRATION_MAP = { 29 | 'bash': BashTerminalIntegration 30 | } 31 | -------------------------------------------------------------------------------- /src/python_shell/shell/terminal/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import abc 26 | 27 | from .interfaces import ITerminalIntegration 28 | 29 | 30 | __all__ = ('BaseTerminalIntegration',) 31 | 32 | 33 | class BaseTerminalIntegration(ITerminalIntegration): 34 | """Base class for terminal integrations""" 35 | 36 | _shell_name = None # Shell name, like 'bash', 'zsh', etc. 37 | 38 | @property 39 | def shell_name(self): 40 | """Returns a name of shell used in this integration""" 41 | return self._shell_name 42 | 43 | @property 44 | @abc.abstractmethod 45 | def available_commands(self): 46 | """Returns list of available executable commands in the shell""" 47 | raise NotImplementedError 48 | -------------------------------------------------------------------------------- /src/python_shell/shell/terminal/bash.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from python_shell.shell.terminal.base import BaseTerminalIntegration 26 | from python_shell.util import SyncProcess 27 | from python_shell.util import Subprocess 28 | from python_shell.util.streaming import decode_stream 29 | 30 | 31 | __all__ = ('BashTerminalIntegration',) 32 | 33 | 34 | class BashTerminalIntegration(BaseTerminalIntegration): 35 | """Terminal integration for Bash""" 36 | 37 | _shell_name = "bash" 38 | _available_commands = None 39 | 40 | def __init__(self): 41 | super(BashTerminalIntegration, self).__init__() 42 | 43 | def _get_available_commands(self): 44 | """Reload available commands from shell""" 45 | process = SyncProcess( 46 | self._shell_name, '-c', 'compgen -c', 47 | stdout=Subprocess.PIPE, 48 | stderr=Subprocess.DEVNULL, 49 | check=True 50 | ) 51 | process.execute() 52 | return decode_stream(process.stdout).split() 53 | 54 | @property 55 | def available_commands(self): 56 | """Returns list of available executable commands in the shell""" 57 | if self._available_commands is None: 58 | self._available_commands = self._get_available_commands() 59 | return self._available_commands 60 | -------------------------------------------------------------------------------- /src/python_shell/shell/terminal/interfaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import abc 26 | 27 | import six 28 | 29 | 30 | __all__ = ('ITerminalIntegration',) 31 | 32 | 33 | class ITerminalIntegration(six.with_metaclass(abc.ABCMeta)): 34 | """Interface for defining integration with Terminal""" 35 | 36 | @property 37 | @abc.abstractmethod 38 | def available_commands(self): 39 | """Returns list of available executable commands in the shell""" 40 | raise NotImplementedError 41 | 42 | @property 43 | @abc.abstractmethod 44 | def shell_name(self): 45 | """Returns a name of shell used in this integration""" 46 | raise NotImplementedError 47 | -------------------------------------------------------------------------------- /src/python_shell/util/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from python_shell.shell.processing.process import * 26 | from .terminal import * 27 | from .version import * 28 | 29 | 30 | __all__ = ( 31 | 'is_python2_running', 32 | 'get_current_terminal_name', 33 | 'Subprocess', 34 | 'Process', 35 | 'SyncProcess', 36 | 'AsyncProcess' 37 | ) 38 | -------------------------------------------------------------------------------- /src/python_shell/util/streaming.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | __all__ = ('decode_stream',) 26 | 27 | 28 | def decode_stream(stream): 29 | """Decodes stream and returns as a single string""" 30 | 31 | return ''.join(map(lambda s: s.decode(), stream)) 32 | -------------------------------------------------------------------------------- /src/python_shell/util/terminal.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import os 26 | 27 | 28 | __all__ = ('get_current_terminal_name',) 29 | 30 | 31 | def get_current_terminal_name(): 32 | """Retrieve name of currently active terminal 33 | 34 | NOTE(albartash): Currently retrieves name using $SHELL variable which 35 | is not quite good. Also not sure if work on Windows. 36 | TODO(albartash): Replace logic for better one to retrieve a proper 37 | terminal name 38 | """ 39 | return os.environ['SHELL'].split('/')[-1] 40 | -------------------------------------------------------------------------------- /src/python_shell/util/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from sys import version_info 26 | 27 | 28 | __all__ = ('is_python2_running',) 29 | 30 | 31 | def __get_python_version(): 32 | """Retrieve current Python X.Y version as (X, Y)""" 33 | return version_info[0:2] 34 | 35 | 36 | def is_python2_running(): 37 | """Check if current interpreter is Python 2.x""" 38 | return __get_python_version()[0] == 2 39 | -------------------------------------------------------------------------------- /src/python_shell/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import pkg_resources 26 | 27 | 28 | __all__ = ('get_version',) 29 | 30 | 31 | def get_version(): 32 | """Retrieves version of the current root package""" 33 | 34 | return pkg_resources.require('python_shell')[0].version.strip() 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | -------------------------------------------------------------------------------- /tests/fixtures/interfaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from python_shell.command.interfaces import ICommand 26 | from python_shell.shell.processing.interfaces import IProcess 27 | from python_shell.shell.terminal.interfaces import ITerminalIntegration 28 | 29 | 30 | __all__ = ('FakeCommand', 'FakeProcess', 'FakeTerminal') 31 | 32 | 33 | class FakeCommand(ICommand): 34 | """Fake command for testing interfaces""" 35 | 36 | def __init__(self): 37 | pass 38 | 39 | @property 40 | def command(self): 41 | return super(FakeCommand, self).command 42 | 43 | @property 44 | def arguments(self): 45 | return super(FakeCommand, self).arguments 46 | 47 | @property 48 | def output(self): 49 | return super(FakeCommand, self).output 50 | 51 | @property 52 | def errors(self): 53 | return super(FakeCommand, self).errors 54 | 55 | @property 56 | def return_code(self): 57 | return super(FakeCommand, self).return_code 58 | 59 | 60 | class FakeTerminal(ITerminalIntegration): 61 | """Fake terminal for testing terminal integration interface""" 62 | 63 | @property 64 | def available_commands(self): 65 | return super(FakeTerminal, self).available_commands 66 | 67 | @property 68 | def shell_name(self): 69 | return super(FakeTerminal, self).shell_name 70 | 71 | 72 | class FakeProcess(IProcess): 73 | """Fake process for testing process interface""" 74 | 75 | @property 76 | def stderr(self): 77 | return super(FakeProcess, self).stderr 78 | 79 | @property 80 | def stdout(self): 81 | return super(FakeProcess, self).stdout 82 | 83 | @property 84 | def returncode(self): 85 | return super(FakeProcess, self).returncode 86 | 87 | @property 88 | def is_finished(self): 89 | return super(FakeProcess, self).is_finished 90 | 91 | def execute(self): 92 | return super(FakeProcess, self).execute() 93 | 94 | @property 95 | def is_undefined(self): 96 | return super(FakeProcess, self).is_undefined 97 | -------------------------------------------------------------------------------- /tests/test_command.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import os 26 | import tempfile 27 | import time 28 | import unittest 29 | 30 | from python_shell.command import Command 31 | from python_shell.exceptions import CommandDoesNotExist 32 | from python_shell.util.streaming import decode_stream 33 | 34 | 35 | __all__ = ('CommandTestCase',) 36 | 37 | 38 | class CommandTestCase(unittest.TestCase): 39 | FILES_COUNT = 5 40 | 41 | def setUp(self): 42 | self.tmp_folder = tempfile.mkdtemp() 43 | 44 | def tearDown(self): 45 | os.rmdir(self.tmp_folder) 46 | 47 | def test_existing_command(self): 48 | """Check that existing command runs correctly""" 49 | 50 | command = Command('ls') 51 | command(self.tmp_folder) 52 | self.assertEqual(command.return_code, 0) 53 | 54 | def test_non_existing_command(self): 55 | """Check when command does not exist""" 56 | with self.assertRaises(CommandDoesNotExist): 57 | Command('random_{}'.format(time.time()))() 58 | 59 | def test_command_output(self): 60 | """Check command output property""" 61 | value = str(time.time()) 62 | command = Command('echo')(value) 63 | output = decode_stream(command.output) 64 | self.assertEqual(output, "{}\n".format(value)) 65 | 66 | def test_string_representation(self): 67 | """Check command string representation""" 68 | value = str(time.time()) 69 | cmd = 'echo' 70 | command = Command(cmd)(value) 71 | self.assertEqual(str(command), "{} {}".format(cmd, value)) 72 | 73 | def test_command_base_representation(self): 74 | """Check command general representation""" 75 | args = ('-l', '-a', '/tmp') 76 | command = Command('ls')(*args) 77 | self.assertEqual(repr(command), ' '.join((command.command,) + args)) 78 | 79 | def test_command_properties(self): 80 | """Check all command properties""" 81 | 82 | cmd_name = 'ls' 83 | args = ('-l', '/tmp') 84 | cmd = Command(cmd_name)(*args) 85 | 86 | self.assertEqual(cmd.command, cmd_name) 87 | self.assertEqual(cmd.arguments, ' '.join(args)) 88 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import time 26 | import unittest 27 | 28 | from python_shell.command import Command 29 | from python_shell.shell.processing.process import AsyncProcess 30 | from python_shell.shell.processing.process import SyncProcess 31 | from python_shell import exceptions 32 | 33 | 34 | __all__ = ('ExceptionTestCase',) 35 | 36 | 37 | class ExceptionTestCase(unittest.TestCase): 38 | """Tests for exceptions classes""" 39 | 40 | def test_command_does_not_exist(self): 41 | """Check that CommandDoesNotExist works properly""" 42 | 43 | cmd_name = "test_{}".format(time.time()) 44 | with self.assertRaises(exceptions.CommandDoesNotExist) as context: 45 | cmd = Command(cmd_name) 46 | raise exceptions.CommandDoesNotExist(cmd) 47 | self.assertEqual(str(context.exception), 48 | 'Command "{}" does not exist'.format(cmd_name)) 49 | 50 | def test_undefined_process(self): 51 | """Check exception for Undefined process""" 52 | 53 | for method in ('wait', 'terminate'): 54 | for process_cls in (SyncProcess, AsyncProcess): 55 | try: 56 | process = process_cls('ls') 57 | getattr(process, method)() 58 | except exceptions.UndefinedProcess as e: 59 | self.assertEqual(str(e), 60 | "Undefined process cannot be used") 61 | else: 62 | self.fail("UndefinedProcess was not thrown") 63 | 64 | def test_run_process_error(self): 65 | """Check exception for running process""" 66 | 67 | for process_cls in (SyncProcess, AsyncProcess): 68 | try: 69 | process = process_cls('sleepa', 'asd') 70 | process.execute() 71 | except exceptions.RunProcessError as e: 72 | error = "Fail to run 'sleepa asd'" 73 | self.assertEqual(error, str(e)) 74 | else: 75 | self.fail("RunProcessError was not thrown") 76 | -------------------------------------------------------------------------------- /tests/test_interfaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import unittest 26 | 27 | from .fixtures import interfaces 28 | 29 | 30 | __all__ = ('CommandInterfaceTestCase', 'TerminalInterfaceTestCase') 31 | 32 | 33 | class BaseInterfaceTestCase(unittest.TestCase): 34 | """Base test case for interfaces""" 35 | implementation_class = None # Interface fake implementation class 36 | properties = () # List of properties to be checked 37 | 38 | @classmethod 39 | def setUpClass(cls): 40 | """Initialize implementation""" 41 | super(BaseInterfaceTestCase, cls).setUpClass() 42 | cls._instance = cls.implementation_class() 43 | 44 | def _check_properties(self): 45 | """Check that all properties are abstract""" 46 | for prop_name in self.properties: 47 | with self.assertRaises(NotImplementedError): 48 | getattr(self._instance, prop_name) 49 | 50 | 51 | class CommandInterfaceTestCase(BaseInterfaceTestCase): 52 | """Test case for Command interface""" 53 | 54 | implementation_class = interfaces.FakeCommand 55 | properties = ('output', 'arguments', 'command', 'return_code', 'errors') 56 | 57 | def test_command_interface(self): 58 | self._check_properties() 59 | 60 | 61 | class TerminalInterfaceTestCase(BaseInterfaceTestCase): 62 | """Test case for Terminal integration interface""" 63 | 64 | implementation_class = interfaces.FakeTerminal 65 | properties = ('available_commands', 'shell_name') 66 | 67 | def test_terminal_integration_interface(self): 68 | self._check_properties() 69 | 70 | 71 | class ProcessInterfaceTestCase(BaseInterfaceTestCase): 72 | """Test case for Process interface""" 73 | 74 | implementation_class = interfaces.FakeProcess 75 | properties = ('stderr', 'stdout', 'returncode', 'is_finished', 76 | 'is_undefined') 77 | 78 | def test_process_interface(self): 79 | self._check_properties() 80 | 81 | with self.assertRaises(NotImplementedError): 82 | self.implementation_class().execute() 83 | -------------------------------------------------------------------------------- /tests/test_process.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import time 26 | import unittest 27 | 28 | from python_shell.shell.processing.process import AsyncProcess 29 | from python_shell.shell.processing.process import Process 30 | from python_shell.shell.processing.process import StreamIterator 31 | from python_shell.shell.processing.process import SyncProcess 32 | from python_shell.shell.processing.process import Subprocess 33 | from python_shell.util.streaming import decode_stream 34 | 35 | 36 | class FakeBaseProcess(Process): 37 | """Fake Process implementation""" 38 | 39 | def execute(self): 40 | """Wrapper for running execute() of parent""" 41 | 42 | return super(FakeBaseProcess, self).execute() 43 | 44 | 45 | class SyncProcessTestCase(unittest.TestCase): 46 | """Test case for synchronous process wrapper""" 47 | 48 | processes = [] 49 | 50 | def tearDown(self): 51 | """Cleanup processes""" 52 | 53 | for p in self.processes: 54 | if not p._process: 55 | continue 56 | try: 57 | p._process.terminate() 58 | p._process.wait() 59 | except OSError: 60 | pass 61 | p._process.stderr and p._process.stderr.close() 62 | p._process.stdout and p._process.stdout.close() 63 | 64 | def _test_sync_process_is_finished(self): 65 | sync_process_args = ['echo', 'Hello'] 66 | sync_process_kwargs = { 67 | 'stdout': Subprocess.DEVNULL, 68 | 'stderr': Subprocess.DEVNULL 69 | } 70 | process = SyncProcess(*sync_process_args, 71 | **sync_process_kwargs) 72 | self.processes.append(process) 73 | 74 | self.assertIsNone(process.returncode) 75 | self.assertIsNone(process.is_finished) 76 | self.assertIsNone(process.is_terminated) 77 | 78 | process.execute() 79 | self.assertIsNotNone(process.returncode) 80 | self.assertTrue(process.is_finished) 81 | 82 | def _test_sync_process_not_initialized(self): 83 | """Check process which was not initialized""" 84 | process = SyncProcess(['ls']) 85 | self.processes.append(process) 86 | self.assertTrue(process.is_undefined) 87 | 88 | def test_sync_process_property_is_finished(self): 89 | """Check that is_finished works well for SyncProcess""" 90 | self._test_sync_process_is_finished() 91 | # TODO(albartash): Check for not finished process is TBD: 92 | # It needs a proper implementation, 93 | # as SyncProcess blocks main thread. 94 | self._test_sync_process_not_initialized() 95 | 96 | def test_sync_process_termination(self): 97 | """Check that SyncProcess can be terminated properly""" 98 | self.skipTest("TODO") 99 | 100 | def test_sync_process_completion(self): 101 | """Check that SyncProcess can be completed properly""" 102 | self.skipTest("TODO") 103 | 104 | 105 | class AsyncProcessTestCase(unittest.TestCase): 106 | """Test case for asynchronous process wrapper""" 107 | 108 | processes = [] 109 | 110 | def tearDown(self): 111 | """Cleanup processes""" 112 | for p in self.processes: 113 | if not p._process: 114 | continue 115 | try: 116 | p._process.terminate() 117 | p._process.wait() 118 | except OSError: 119 | pass 120 | p._process.stderr and p._process.stderr.close() 121 | p._process.stdout and p._process.stdout.close() 122 | 123 | def test_async_process_is_finished(self): 124 | timeout = 0.1 # seconds 125 | process = AsyncProcess('sleep', str(timeout)) 126 | self.processes.append(process) 127 | 128 | self.assertIsNone(process.returncode) 129 | self.assertIsNone(process.is_finished) 130 | self.assertIsNone(process.is_terminated) 131 | 132 | process.execute() 133 | self.assertIsNone(process.returncode) 134 | time.sleep(timeout + 1) # ensure command finishes 135 | self.assertEqual(process.returncode, 0) 136 | 137 | def test_async_process_is_not_initialized(self): 138 | """Check that async process is not initialized when not finished""" 139 | timeout = 0.5 # seconds 140 | process = AsyncProcess('sleep', str(timeout)) 141 | self.processes.append(process) 142 | self.assertTrue(process.is_undefined) 143 | process.execute() 144 | self.assertIsNone(process.returncode) 145 | time.sleep(timeout + 0.5) 146 | self.assertIsNotNone(process.returncode) 147 | 148 | def test_async_std_properties_accessible(self): 149 | """Check if standard properties are accessible for AsyncProcess""" 150 | 151 | timeout = 0.5 # seconds 152 | process = AsyncProcess('sleep', str(timeout)) 153 | self.processes.append(process) 154 | process.execute() 155 | stdout = decode_stream(process.stdout) 156 | stderr = decode_stream(process.stderr) 157 | 158 | self.assertEqual(stdout, "") 159 | self.assertEqual(stderr, "") 160 | 161 | def test_async_process_property_is_finished(self): 162 | self.skipTest("TODO") 163 | 164 | def test_async_process_termination(self): 165 | """Check that AsyncProcess can be terminated properly""" 166 | 167 | process = AsyncProcess('yes') 168 | self.processes.append(process) 169 | process.execute() 170 | process.terminate() 171 | 172 | self.assertTrue(process.is_terminated) 173 | self.assertEqual(process.returncode, -15) 174 | 175 | def test_async_process_completion(self): 176 | """Check that AsyncProcess can be completed properly""" 177 | 178 | timeout = str(0.5) 179 | process = AsyncProcess('sleep', timeout) 180 | self.processes.append(process) 181 | process.execute() 182 | process.wait() 183 | self.assertTrue(process.is_finished) 184 | self.assertEqual(process.returncode, 0) 185 | 186 | 187 | class ProcessTestCase(unittest.TestCase): 188 | """Test case for Process class""" 189 | 190 | def test_execution_of_base_process(self): 191 | """Check execution of Process instance""" 192 | 193 | with self.assertRaises(NotImplementedError): 194 | FakeBaseProcess(None).execute() 195 | 196 | 197 | class StreamIteratorTestCase(unittest.TestCase): 198 | """Test case for StreamIterator instance""" 199 | 200 | def test_stream_is_not_set(self): 201 | """Check work of iterator when stream is not passed""" 202 | 203 | stream = StreamIterator(stream=None) 204 | with self.assertRaises(StopIteration): 205 | next(stream) 206 | -------------------------------------------------------------------------------- /tests/test_shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import time 26 | import unittest 27 | 28 | 29 | from python_shell.exceptions import CommandDoesNotExist 30 | from python_shell.exceptions import ShellException 31 | from python_shell.shell import Shell 32 | from python_shell.shell.terminal import TERMINAL_INTEGRATION_MAP 33 | from python_shell.util.streaming import decode_stream 34 | from python_shell.util.terminal import get_current_terminal_name 35 | 36 | 37 | __all__ = ('ShellTestCase',) 38 | 39 | 40 | class ShellTestCase(unittest.TestCase): 41 | """Test case for Shell""" 42 | 43 | def test_own_fields(self): 44 | """Check Shell own fields to be accessible""" 45 | for field in ('last_command',): 46 | self.assertIsNotNone(getattr(Shell, field)) 47 | 48 | def test_shell_non_zero_return_code(self): 49 | """Check the case when Shell command returns non-zero code""" 50 | with self.assertRaises(ShellException) as context: 51 | Shell.mkdir('/tmp') 52 | self.assertEqual(str(context.exception), 53 | 'Shell command "mkdir /tmp" failed ' 54 | 'with return code 1') 55 | 56 | def test_last_command(self): 57 | """Check "last_command" property to be working""" 58 | command = Shell.mkdir('-p', '/tmp') 59 | self.assertEqual(Shell.last_command.command, 'mkdir') 60 | self.assertEqual(Shell.last_command.arguments, '-p /tmp') 61 | self.assertEqual(command, Shell.last_command) 62 | 63 | def test_command_errors(self): 64 | """Check command errors property""" 65 | command = Shell.ls 66 | non_existing_dir_name = "/nofolder_{:.0f}".format(time.time()) 67 | with self.assertRaises(ShellException): 68 | command(non_existing_dir_name) 69 | 70 | # NOTE(albartash): This test partially relies on "ls" output, 71 | # but it's done as less as possible 72 | error_output = decode_stream(command.errors) 73 | for part in ('ls', non_existing_dir_name, 'No such'): 74 | self.assertIn(part, error_output) 75 | 76 | def test_dir_shell(self): 77 | """Check usage of dir(Shell)""" 78 | name = get_current_terminal_name() 79 | commands = TERMINAL_INTEGRATION_MAP[name]().available_commands 80 | self.assertLess(0, len(commands)) 81 | commands_dir = dir(Shell) 82 | self.assertEqual(sorted(commands + ['last_command']), commands_dir) 83 | 84 | def test_shell_for_non_identifier_command(self): 85 | """Check ability to call Shell for non-identifier-like commands""" 86 | command_name = '2echo' 87 | command = Shell(command_name) 88 | with self.assertRaises(CommandDoesNotExist): 89 | command() 90 | self.assertEqual(Shell.last_command.command, command_name) 91 | -------------------------------------------------------------------------------- /tests/test_terminal_integrations.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import unittest 26 | 27 | from python_shell.shell import terminal 28 | from python_shell.shell.terminal.base import BaseTerminalIntegration 29 | 30 | 31 | __all__ = ('TerminalIntegrationTestCase',) 32 | 33 | 34 | class FakeBaseTerminal(BaseTerminalIntegration): 35 | """Fake terminal integration for BaseTerminalIntegration""" 36 | 37 | @property 38 | def available_commands(self): 39 | """Wrapper to call parent's property""" 40 | 41 | return super(FakeBaseTerminal, self).available_commands 42 | 43 | 44 | class TerminalIntegrationTestCase(unittest.TestCase): 45 | """Abstract Test case for terminal integration""" 46 | 47 | def _test_terminal_available_commands(self, integration_class_name): 48 | """Check available commands to be non-empty list""" 49 | term = getattr(terminal, integration_class_name)() 50 | self.assertLess(0, len(term.available_commands)) 51 | 52 | 53 | class BaseTerminalIntegrationTestCase(TerminalIntegrationTestCase): 54 | """Test case for Base terminal integration class""" 55 | 56 | def test_shell_name(self): 57 | """Check 'shell_name' property for Base terminal class""" 58 | 59 | self.assertIsNone(FakeBaseTerminal().shell_name) 60 | 61 | def test_available_commands(self): 62 | """Check 'available_commands' property for Base terminal class""" 63 | 64 | with self.assertRaises(NotImplementedError): 65 | _ = FakeBaseTerminal().available_commands 66 | 67 | 68 | class BashTerminalIntegrationTestCase(TerminalIntegrationTestCase): 69 | """Test case for Bash terminal integration""" 70 | 71 | def test_bash_available_commands(self): 72 | """Check if Bash available commands can be retrieved""" 73 | self._test_terminal_available_commands('BashTerminalIntegration') 74 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2020 Alex Sokolov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import sys 26 | import unittest 27 | 28 | from python_shell.shell.terminal import TERMINAL_INTEGRATION_MAP 29 | from python_shell.util import is_python2_running 30 | from python_shell.util import get_current_terminal_name 31 | 32 | 33 | __all__ = ('UtilTestCase',) 34 | 35 | 36 | class UtilTestCase(unittest.TestCase): 37 | """Test case for utils""" 38 | 39 | def test_python_version_checker(self): 40 | """Check if python version checker works properly""" 41 | self.assertEqual(is_python2_running(), sys.version_info[0] == 2) 42 | 43 | def test_get_current_terminal_name(self): 44 | """Check that getting current terminal name works""" 45 | self.assertIn(get_current_terminal_name(), 46 | TERMINAL_INTEGRATION_MAP.keys()) 47 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Alex Sokolov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | [tox] 24 | envlist = 25 | py27 26 | py35,py36,py37,py38,py39 27 | pypy,pypy3 28 | coverage,coverage27 29 | pep8 30 | pylint,pylint27 31 | docs 32 | 33 | 34 | [testenv] 35 | passenv = SHELL 36 | commands = python -m unittest 37 | install_command = 38 | pip install -U {opts} {packages} 39 | # For enabling tracemalloc, replace command above with 40 | # commands = python -X tracemalloc -m unittest 41 | 42 | 43 | [testenv:py27] 44 | commands = python -m unittest discover -p "test_*.py" 45 | 46 | 47 | [testenv:pep8] 48 | deps = pycodestyle 49 | flake8 50 | commands = pycodestyle --exclude=.tox,.venv*,.env*, . 51 | flake8 --exclude=.tox,.venv*,.env* --ignore=F403,F405 52 | 53 | 54 | [testenv:coverage] 55 | deps = coverage 56 | commands = 57 | coverage erase 58 | coverage run -p setup.py test 59 | coverage combine 60 | coverage report 61 | 62 | 63 | [testenv:coverage27] 64 | basepython = python2.7 65 | deps = coverage 66 | commands = 67 | coverage erase 68 | coverage run -p setup.py test 69 | coverage combine 70 | coverage report 71 | 72 | 73 | [testenv:pylint] 74 | deps = pylint 75 | commands = 76 | pylint python_shell 77 | 78 | 79 | [testenv:pylint27] 80 | basepython = python2.7 81 | deps = pylint 82 | commands = 83 | pylint python_shell 84 | 85 | 86 | [testenv:docs] 87 | description = Run a development server for working on documentation 88 | basepython = python3.7 89 | deps = mkdocs >= 1.1, < 2 90 | mkdocs-material 91 | commands = mkdocs build --clean 92 | python -c 'print("###### Starting local server. Press Control+C to stop server ######")' 93 | mkdocs serve -a localhost:8080 94 | --------------------------------------------------------------------------------