├── VERSION
├── MANIFEST.in
├── docs
├── tutorial
│ └── tutorial.md
├── index.md
├── user-guide
│ ├── integrations.md
│ ├── testing.md
│ ├── getting-started.md
│ └── installation.md
├── license.md
└── releases.md
├── .gitignore
├── mkdocs.yml
├── CHANGELOG.md
├── LICENSE
├── tests
├── __init__.py
├── fixtures
│ ├── __init__.py
│ └── interfaces.py
├── test_util.py
├── test_terminal_integrations.py
├── test_interfaces.py
├── test_exceptions.py
├── test_command.py
├── test_shell.py
└── test_process.py
├── .coveragerc
├── src
└── python_shell
│ ├── shell
│ ├── __init__.py
│ ├── terminal
│ │ ├── __init__.py
│ │ ├── interfaces.py
│ │ ├── base.py
│ │ └── bash.py
│ ├── processing
│ │ ├── __init__.py
│ │ ├── interfaces.py
│ │ └── process.py
│ └── core.py
│ ├── __init__.py
│ ├── command
│ ├── __init__.py
│ ├── interfaces.py
│ └── command.py
│ ├── exceptions
│ ├── base.py
│ ├── __init__.py
│ ├── shell.py
│ └── process.py
│ ├── util
│ ├── streaming.py
│ ├── __init__.py
│ ├── version.py
│ └── terminal.py
│ └── version.py
├── tox.ini
├── setup.py
└── README.md
/VERSION:
--------------------------------------------------------------------------------
1 | 1.0.4
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include VERSION
2 |
--------------------------------------------------------------------------------
/docs/tutorial/tutorial.md:
--------------------------------------------------------------------------------
1 | This space is in development. Will be published in the next release.
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/**
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/__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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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_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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------