From 453c1f9bdd3004771935ca3d6cabeef211df1c6a Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Fri, 26 Feb 2021 17:28:50 -0500 Subject: [PATCH 1/7] Convert to tox, fix typo, clean up tests --- .github/workflows/pythonpackage.yml | 11 ++- Makefile | 67 ------------------- README.md | 2 +- dev-requirements.txt | 9 ++- forge/cli.py | 11 +-- forge/forge.py | 12 ++-- forge/pipx_wrapper.py | 16 ++--- setup.cfg | 4 +- test.ps1 | 11 --- tests/cli_test.py | 100 +++++++++++++++++++--------- tests/conftest.py | 33 ++++++++- tests/forge_test.py | 38 +++++------ tests/main_test.py | 16 +++-- tests/pipx_wrapper_test.py | 93 +++++++++++++++----------- tox.ini | 47 +++++++++++++ 15 files changed, 264 insertions(+), 206 deletions(-) delete mode 100644 Makefile delete mode 100644 test.ps1 create mode 100644 tox.ini diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index ecb11e9..46f0306 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -18,13 +18,10 @@ jobs: python-version: [3.7, 3.8, 3.9] include: - os: ubuntu-latest - test_script: make test cache_path: ~/.cache/pip - os: macos-latest - test_script: make test cache_path: ~/Library/Caches/pip - os: windows-latest - test_script: .\test.ps1 cache_path: ~\AppData\Local\pip\Cache steps: @@ -38,6 +35,8 @@ jobs: path: ${{ matrix.cache_path }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | - ${{ runner.os }}-pip- - - name: Lint with pylint and test with pytest - run: ${{ matrix.test_script }} + ${{ runner.os }}-pip- + - name: Install tox + run: pip install -U tox + - name: Lint, Test, Type Check, Bandit, and Safety + run: tox -e lint,test,bandit,type-check,safety -p diff --git a/Makefile b/Makefile deleted file mode 100644 index 928a7b8..0000000 --- a/Makefile +++ /dev/null @@ -1,67 +0,0 @@ -IMAGE_NAME='Forge' -PYTHON=python - - -.DEFAULT: help -help: - @echo "make init" - @echo " prepare development environment and create virtualenv" - @echo "make test" - @echo " run lint, pytype and unit tests" - @echo "make lint" - @echo " run lint and pytype only" - @echo "make build" - @echo " run lint, test, and build package" - @echo "make clean" - @echo " clean compiled files and the virtual environment" - @echo "\n" - @echo "For Development:" - @echo "make install" - @echo " installs package from source" - -init: - rm -rf .venv - pip3 install virtualenv - virtualenv --python=$(PYTHON) --always-copy .venv - ( \ - . .venv/bin/activate; \ - pip3 install -r requirements.txt; \ - pip3 install -e .; \ - ) - -dev: init - ( \ - . .venv/bin/activate; \ - pip3 install -r dev-requirements.txt; \ - pip3 install mypy==0.790; \ - ) - - -lint: dev - ( \ - . .venv/bin/activate; \ - $(PYTHON) -m pylint -j 4 -r y forge; \ - ) - -type-check: - ( \ - . .venv/bin/activate; \ - $(PYTHON) -m mypy forge; \ - ) - -build: - ( \ - . .venv/bin/activate; \ - pip3 install --upgrade setuptools wheel; \ - $(PYTHON) setup.py sdist bdist_wheel; \ - ) - -test: lint type-check - ( \ - . .venv/bin/activate; \ - $(PYTHON) -m pytest -rf -vvv -x --count 5 --cov=forge --cov-fail-under=80 --cov-report term-missing; \ - ) - - -clean: - rm -rf forge.egg-info/ build/ dist/ .venv/ venv/ **/__pycache__/ .pytest_cache/ .coverage \ No newline at end of file diff --git a/README.md b/README.md index 6904a0b..1f43734 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ python -m pipx ensurepath --force ## Installation -> **NOTE:** Befrore moving on make sure the envirnonment variables are set, open a new terminal and run +> **NOTE:** Before moving on make sure the envirnonment variables are set, open a new terminal and run > `echo $PIPX_HOME` or `$env:PIPX_HOME` diff --git a/dev-requirements.txt b/dev-requirements.txt index 17f9038..e387177 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,6 @@ -pylint==2.6.0 -pytest==6.1.2 +pylint==2.7.1 +pytest==6.2.2 pytest-randomly==3.5.0 pytest-repeat==0.9.1 -pytest-cov==2.10.1 -mock==4.0.2 -twine==1.13.0 \ No newline at end of file +pytest-cov==2.11.1 +mock==4.0.3 \ No newline at end of file diff --git a/forge/cli.py b/forge/cli.py index 7f1466b..04266e2 100644 --- a/forge/cli.py +++ b/forge/cli.py @@ -1,14 +1,15 @@ """ Forge CLI """ -from typing import List -from subprocess import Popen import sys +import subprocess +from typing import List + import click import pkg_resources from forge import forge - -from .pipx_wrapper import install_to_pipx, uninstall_from_pipx, update_pipx +from forge.pipx_wrapper import (install_to_pipx, uninstall_from_pipx, + update_pipx) CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], ignore_unknown_options=True, @@ -102,7 +103,7 @@ def list_forge_plugins() -> None: def run_forge_plugin(command: List[str]) -> None: """ Forge Plugin """ - process = Popen(command) + process = subprocess.Popen(command) stdout, stderr = process.communicate() if stdout: diff --git a/forge/forge.py b/forge/forge.py index ce7fdbf..7c37dd8 100644 --- a/forge/forge.py +++ b/forge/forge.py @@ -3,12 +3,12 @@ import os from json import loads from pathlib import Path -from typing import Dict, List, Any +from typing import Any, Dict, List -from tabulate import tabulate -from halo import Halo +import tabulate +import halo -from .exceptions import PluginManagementFatalException +from forge.exceptions import PluginManagementFatalException FORGE_PATH = os.path.join(Path.home(), '.forge') PLUGIN_PATH = os.path.join(FORGE_PATH, 'venvs') @@ -76,6 +76,6 @@ def list_plugins() -> None: for config in get_plugins()] if len(tabulated_data) == 0: - Halo().warn('No forge plugins installed yet! - Run forge --help for help') + halo.Halo().warn('No forge plugins installed yet! - Run forge --help for help') else: - print(tabulate(tabulated_data, ['plugin', 'version']), end='\n\n') + print(tabulate.tabulate(tabulated_data, ['plugin', 'version']), end='\n\n') diff --git a/forge/pipx_wrapper.py b/forge/pipx_wrapper.py index 124d7ea..b927e1e 100644 --- a/forge/pipx_wrapper.py +++ b/forge/pipx_wrapper.py @@ -2,13 +2,13 @@ import re -from subprocess import PIPE, CalledProcessError, Popen +import subprocess from typing import List, Tuple -from halo import Halo +import halo -from .exceptions import (PluginManagementFatalException, - PluginManagementWarnException) +from forge.exceptions import (PluginManagementFatalException, + PluginManagementWarnException) DOTS = { "interval": 80, @@ -16,9 +16,9 @@ } -def make_spinner(text: str) -> Halo: +def make_spinner(text: str) -> halo.Halo: """ Creates uniform stylized Halo spinner """ - return Halo( + return halo.Halo( text=text, spinner=DOTS, color='blue' @@ -42,7 +42,7 @@ def determine_is_fatal_error(current_error_message: str) -> bool: def run_command(command: List[str]) -> Tuple[str, str]: """ Wrapper to simplify handling subprocess commands """ try: - process = Popen(command, stdout=PIPE, stderr=PIPE) + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() @@ -51,7 +51,7 @@ def run_command(command: List[str]) -> Tuple[str, str]: return stdout.decode(), stderr.decode() - except CalledProcessError as err: + except subprocess.CalledProcessError as err: raise PluginManagementFatalException(err) from None diff --git a/setup.cfg b/setup.cfg index 7435177..96ff747 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,13 @@ [mypy] warn_redundant_casts = True warn_unused_ignores = True +warn_unreachable = True disallow_subclassing_any = False ignore_missing_imports = True disallow_untyped_calls = True disallow_untyped_defs = True +disallow-incomplete-defs = True check_untyped_defs = True no_implicit_optional = True -strict_optional = True \ No newline at end of file +strict_optional = True diff --git a/test.ps1 b/test.ps1 deleted file mode 100644 index 1742e57..0000000 --- a/test.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -$ErrorActionPreference = "Stop" -python -m pip install virtualenv -virtualenv.exe --always-copy venv -. venv/Scripts/activate -pip3 install -r requirements.txt -pip3 install -r dev-requirements.txt -pip3 install -e . - -python -m pylint -j 4 -r y forge -python -m pytest -rf -vvv -x --count 5 --cov=forge --cov-fail-under=80 --cov-report term -deactivate \ No newline at end of file diff --git a/tests/cli_test.py b/tests/cli_test.py index 2d29c91..cfc1127 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -3,147 +3,184 @@ from mock import call, patch -@patch('forge.forge.list_plugins') +@pytest.fixture() +def mock_pipx_list(): + with patch('forge.forge.list_plugins') as mock: + yield mock + + +@pytest.fixture() +def mock_pipx_install(): + with patch('forge.cli.install_to_pipx') as mock: + yield mock + + +@pytest.fixture() +def mock_pipx_update(): + with patch('forge.cli.update_pipx') as mock: + yield mock + + +@pytest.fixture() +def mock_get_plugins(): + with patch('forge.forge.get_plugins') as mock: + yield mock + + +@pytest.fixture() +def mock_uninstall_from_pipx(): + with patch('forge.cli.uninstall_from_pipx') as mock: + yield mock + + def test_cli_no_command(mock_pipx_list): mock_args = 'forge'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + mock_pipx_list.assert_called_once_with() -@patch('forge.forge.list_plugins') def test_cli_list(mock_pipx_list): mock_args = 'forge list'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + mock_pipx_list.assert_called_once_with() -@patch('forge.cli.install_to_pipx') def test_cli_add(mock_pipx_install): mock_args = 'forge add --source some-source'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + mock_pipx_install.assert_called_once_with(source='some-source', extra_args=[]) -@patch('forge.cli.install_to_pipx') def test_cli_add_with_extra_args(mock_pipx_install): mock_args = 'forge add -s some-source arg1 val1 arg2 val2'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + mock_pipx_install.assert_called_once_with( source='some-source', extra_args=['arg1', 'val1', 'arg2', 'val2'] ) -@patch('forge.cli.update_pipx') def test_cli_update(mock_pipx_update): mock_args = 'forge update --name plugin-name'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + mock_pipx_update.assert_called_once_with(name='forge-plugin-name', extra_args=[]) -@patch('forge.cli.update_pipx') def test_cli_update_with_extra_args(mock_pipx_update): mock_args = 'forge update -n plugin-name arg1 val1 arg2 val2 '.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + mock_pipx_update.assert_called_once_with( name='forge-plugin-name', extra_args=['arg1', 'val1', 'arg2', 'val2'] ) -@patch('forge.cli.update_pipx') -@patch('forge.forge.get_plugins') def test_cli_update_all_since_no_name_given(mock_get_plugins, mock_pipx_update): mock_get_plugins.return_value = [ {"main_package": {"package": "forge-plugin1"}}, {"main_package": {"package": "forge-plugin2"}} ] mock_args = 'forge update'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + assert mock_pipx_update.mock_calls == [ call(name='forge-plugin1', extra_args=[]), call(name='forge-plugin2', extra_args=[]) ] -@patch('forge.cli.update_pipx') -@patch('forge.forge.get_plugins') def test_cli_update_all_since_no_name_given_with_extra_args(mock_get_plugins, mock_pipx_update): mock_get_plugins.return_value = [ {"main_package": {"package": "forge-plugin1"}}, {"main_package": {"package": "forge-plugin2"}} ] mock_args = 'forge update arg1 val1'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() + assert mock_pipx_update.mock_calls == [ call(name='forge-plugin1', extra_args=['arg1', 'val1']), call(name='forge-plugin2', extra_args=['arg1', 'val1']) ] -@patch('forge.cli.uninstall_from_pipx') -def test_cli_remove(uninstall_from_pipx): +def test_cli_remove(mock_uninstall_from_pipx): mock_args = 'forge remove -n plugin-name'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() - uninstall_from_pipx.assert_called_once_with(plugin_name='forge-plugin-name', extra_args=[]) + + mock_uninstall_from_pipx.assert_called_once_with(plugin_name='forge-plugin-name', extra_args=[]) -@patch('forge.cli.uninstall_from_pipx') -def test_cli_remove_with_extra_args(uninstall_from_pipx): +def test_cli_remove_with_extra_args(mock_uninstall_from_pipx): mock_args = 'forge remove -n plugin-name arg1 val1 arg2 val2 '.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() - uninstall_from_pipx.assert_called_once_with( + + mock_uninstall_from_pipx.assert_called_once_with( plugin_name='forge-plugin-name', extra_args=['arg1', 'val1', 'arg2', 'val2'] ) -@patch('forge.cli.uninstall_from_pipx') -@patch('forge.forge.get_plugins') -def test_cli_remove_all_since_no_name_given(mock_get_plugins, mocked_uninstall_from_pipx): +def test_cli_remove_all_since_no_name_given(mock_get_plugins, mock_uninstall_from_pipx): mock_get_plugins.return_value = [ {"main_package": {"package": "forge-plugin1"}}, {"main_package": {"package": "forge-plugin2"}} ] mock_args = 'forge remove'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() - assert mocked_uninstall_from_pipx.mock_calls == [ + + assert mock_uninstall_from_pipx.mock_calls == [ call(plugin_name='forge-plugin1', extra_args=[]), call(plugin_name='forge-plugin2', extra_args=[]) ] -@patch('forge.cli.uninstall_from_pipx') -@patch('forge.forge.get_plugins') -def test_cli_remove_all_since_no_name_given_with_extra_args(mock_get_plugins, mocked_uninstall_from_pipx): +def test_cli_remove_all_since_no_name_given_with_extra_args(mock_get_plugins, mock_uninstall_from_pipx): mock_get_plugins.return_value = [ {"main_package": {"package": "forge-plugin1"}}, {"main_package": {"package": "forge-plugin2"}} ] mock_args = 'forge remove arg1 val1'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() - assert mocked_uninstall_from_pipx.mock_calls == [ + + assert mock_uninstall_from_pipx.mock_calls == [ call(plugin_name='forge-plugin1', extra_args=['arg1', 'val1']), call(plugin_name='forge-plugin2', extra_args=['arg1', 'val1']) ] -@patch('click.echo') def test_cli_help_forge(mock_echo): - mock_args = 'forge -h'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() @@ -152,10 +189,10 @@ def test_cli_help_forge(mock_echo): @patch('forge.forge.get_forge_plugin_command_names') -@patch('click.echo') -def test_cli_help_forge_core_command(mock_echo, mock_command_names): +def test_cli_help_forge_core_command(mock_command_names, mock_echo): mock_args = 'forge add -h'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): forge_cli() @@ -163,23 +200,22 @@ def test_cli_help_forge_core_command(mock_echo, mock_command_names): assert str(mock_echo.mock_calls[0].args[0]).split('\n')[0] == 'Usage: forge add [OPTIONS] [PIPX_ARGS]...' -@patch('forge.cli.Popen') -@patch('click.echo') -def test_run_forge_plugin(mock_echo, mock_popen): +def test_run_forge_plugin(mock_popen, mock_echo): mock_process = mock_popen.return_value mock_process.returncode = 0 mock_process.communicate.return_value = (b'stdout', b'stderr') with pytest.raises(SystemExit): run_forge_plugin(['ls']) + assert mock_echo.mock_calls == [call('stdout'), call('stderr')] @patch('forge.forge.get_forge_plugin_command_names', return_value=['plugin1']) @patch('forge.cli.run_forge_plugin') def test_cli_help_forge_plugin_command(mock_run_forge_plugin, mock_command_names): - mock_args = 'forge plugin1 -h'.split() + with patch('sys.argv', mock_args), pytest.raises(SystemExit): inject_forge_plugins() forge_cli() diff --git a/tests/conftest.py b/tests/conftest.py index 28d25bc..7c12604 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ -from mock import mock_open +import pytest +from mock import mock_open, patch # Credit to: https://gist.github.com/adammartinez271828/137ae25d0b817da2509c1a96ba37fc56 @@ -17,3 +18,33 @@ def multi_mock_open(*file_contents): mock_opener.side_effect = mock_files return mock_opener + + +@pytest.fixture() +def mock_echo(): + with patch('click.echo') as mock: + yield mock + + +@pytest.fixture() +def mock_listdir(): + with patch('os.listdir') as mock: + yield mock + + +@pytest.fixture() +def mock_tabulate(): + with patch('tabulate.tabulate') as mock: + yield mock + + +@pytest.fixture() +def mock_popen(): + with patch('subprocess.Popen') as mock: + yield mock + + +@pytest.fixture() +def mock_spinner(): + with patch('halo.Halo') as mock: + yield mock diff --git a/tests/forge_test.py b/tests/forge_test.py index e85017d..7df798b 100644 --- a/tests/forge_test.py +++ b/tests/forge_test.py @@ -2,15 +2,13 @@ import pytest from forge import forge -from forge.exceptions import (PluginManagementFatalException, - PluginManagementWarnException) from mock import call, mock_open, patch -from .conftest import multi_mock_open +from tests.conftest import multi_mock_open -@patch('os.listdir', return_value=['plugin1', 'plugin2']) -def test_get_plugin_paths(mocked_listdir): +def test_get_plugin_paths(mock_listdir): + mock_listdir.return_value = ['plugin1', 'plugin2'] forge.PLUGIN_PATH = os.path.join('home', 'user', '.forge', 'venvs') @@ -27,7 +25,6 @@ def test_get_plugin_paths(mocked_listdir): def test_get_pipx_config(): - fake_config_json = '{"a":"something", "b":"another"}' with patch("builtins.open", mock_open(read_data=fake_config_json)): @@ -74,7 +71,6 @@ def test_filer_forge_plugins(): def test_get_command_from_config(): - mock_plugin_config = { 'main_package': { 'apps': [ @@ -85,8 +81,9 @@ def test_get_command_from_config(): assert forge.get_command_from_config(mock_plugin_config) == 'plugin-name' -@patch('os.listdir', return_value=['forge-plugin1', 'forge-plugin2', 'non_plugin']) -def test_get_plugins(mocked_listdir): +def test_get_plugins(mock_listdir): + mock_listdir.return_value = ['forge-plugin1', 'forge-plugin2', 'non_plugin'] + forge.PLUGIN_PATH = os.path.join('home', 'user', '.forge', 'venvs') fake_config_jsons = [ @@ -106,10 +103,9 @@ def test_get_plugins(mocked_listdir): assert filtered_plugin_configs == expected_plugin_configs -@patch('forge.forge.Halo') -@patch('forge.forge.tabulate') -@patch('os.listdir', return_value=['forge-plugin1', 'forge-plugin2', 'non_plugin']) -def test_list_plugins(mocked_listdir, mocked_tabulate, mocked_spinner): +def test_list_plugins(mock_listdir, mock_tabulate, mock_spinner): + mock_listdir.return_value = ['forge-plugin1', 'forge-plugin2', 'non_plugin'] + forge.PLUGIN_PATH = os.path.join('home', 'user', '.forge', 'venvs') fake_config_jsons = [ @@ -121,17 +117,14 @@ def test_list_plugins(mocked_listdir, mocked_tabulate, mocked_spinner): with patch("builtins.open", multi_mock_open(*fake_config_jsons)): forge.list_plugins() - mocked_tabulate.assert_called_once_with( + mock_tabulate.assert_called_once_with( [('plugin1', '1.0.0'), ('plugin2', '0.0.1')], ['plugin', 'version'] ) - mocked_spinner.assert_not_called() + mock_spinner.assert_not_called() -@patch('forge.forge.Halo') -@patch('forge.forge.tabulate') -@patch('os.listdir') -def test_list_plugins_no_plugins_installed(mocked_listdir, mocked_tabulate, mocked_spinner): +def test_list_plugins_no_plugins_installed(mock_listdir, mock_tabulate, mock_spinner): forge.PLUGIN_PATH = os.path.join('home', 'user', '.forge', 'venvs') fake_config_jsons = [] @@ -139,5 +132,8 @@ def test_list_plugins_no_plugins_installed(mocked_listdir, mocked_tabulate, mock with patch("builtins.open", multi_mock_open(*fake_config_jsons)): forge.list_plugins() - mocked_tabulate.assert_not_called() - assert mocked_spinner.mock_calls[1] == call().warn('No forge plugins installed yet! - Run forge --help for help') + mock_tabulate.assert_not_called() + + mock_spinner.assert_has_calls([ + call().warn('No forge plugins installed yet! - Run forge --help for help') + ], any_order=True) diff --git a/tests/main_test.py b/tests/main_test.py index 167d2b5..01f271f 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -5,8 +5,18 @@ from mock import patch -@patch('forge.inject_forge_plugins') -@patch('forge.forge_cli') +@pytest.fixture() +def mock_inject(): + with patch('forge.inject_forge_plugins') as mock: + yield mock + + +@pytest.fixture() +def mock_cli(): + with patch('forge.forge_cli') as mock: + yield mock + + def test_main_fatal_exception(mock_cli, mock_inject): mock_cli.side_effect = PluginManagementFatalException('some fatal message') @@ -19,8 +29,6 @@ def test_main_fatal_exception(mock_cli, mock_inject): assert str(err.value) == '1' -@patch('forge.inject_forge_plugins') -@patch('forge.forge_cli') def test_main_warning_exception(mock_cli, mock_inject): mock_cli.side_effect = PluginManagementWarnException('some fatal message') diff --git a/tests/pipx_wrapper_test.py b/tests/pipx_wrapper_test.py index 714881c..347f19f 100644 --- a/tests/pipx_wrapper_test.py +++ b/tests/pipx_wrapper_test.py @@ -11,64 +11,70 @@ def run_command_fail(*args, **kwargs): raise CalledProcessError(returncode=1, cmd=' '.join(args[0])) -@patch('forge.pipx_wrapper.Popen') +@pytest.fixture() +def mock_run_command(): + with patch('forge.pipx_wrapper.run_command') as mock: + yield mock + + def test_run_command(mock_popen): mock_process = mock_popen.return_value mock_process.returncode = 0 mock_process.communicate.return_value = (b'stdout', b'stderr') stdout, stderr = pipx_wrapper.run_command(['ls']) + assert stdout == 'stdout' assert stderr == 'stderr' -@patch('forge.pipx_wrapper.Popen') def test_run_command_fail_non_zero_exit_code(mock_popen): mock_process = mock_popen.return_value mock_process.returncode = 1 mock_process.communicate.return_value = (b'stdout', b'stderr') + with pytest.raises(PluginManagementFatalException): pipx_wrapper.run_command(['ls']) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_update_pipx(mock_run_command, mock_spinner): mock_run_command.return_value = ('forge-plugin-name updated to some version(', '') pipx_wrapper.update_pipx('forge-plugin-name', []) mock_run_command.assert_called_once_with(['pipx', 'upgrade', 'forge-plugin-name', '--verbose']) - assert mock_spinner.mock_calls[0] == call(text='Updating plugin: [plugin-name]...', - spinner={'interval': 80, 'frames': [ - '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' - ]}, - color='blue') - assert mock_spinner.mock_calls[2] == call().__enter__().succeed('plugin-name updated to some version') + mock_spinner.assert_has_calls([ + call(text='Updating plugin: [plugin-name]...', + spinner={'interval': 80, 'frames': [ + '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' + ]}, + color='blue'), + call().__enter__().succeed('plugin-name updated to some version') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_update_pipx_fail_no_update_message_matched(mock_run_command, mock_spinner): mock_run_command.return_value = ('Some pipx output without update message', '') + with pytest.raises(PluginManagementFatalException): pipx_wrapper.update_pipx('forge-plugin-name', []) - assert mock_spinner.mock_calls[2] == call().__enter__().fail('Something went wrong!\nFailed to find update information from update log!') + mock_spinner.assert_has_calls([ + call().__enter__().fail('Something went wrong!\nFailed to find update information from update log!') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_update_pipx_warn_already_up_to_date(mock_run_command, mock_spinner): mock_run_command.return_value = ('', 'Package is not installed') + with pytest.raises(PluginManagementWarnException): pipx_wrapper.update_pipx('forge-plugin-name', []) - assert mock_spinner.mock_calls[2] == call().__enter__().warn('Plugin not installed! Cannot update!') + mock_spinner.assert_has_calls([ + call().__enter__().warn('Plugin not installed! Cannot update!') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_install_to_pipx(mock_run_command, mock_spinner): pipx_output = """ @@ -78,66 +84,77 @@ def test_install_to_pipx(mock_run_command, mock_spinner): """ mock_run_command.return_value = (pipx_output, '') + pipx_wrapper.install_to_pipx('some-source', []) mock_run_command.assert_called_once_with(['pipx', 'install', 'some-source', '--verbose']) + assert mock_spinner.mock_calls[0].kwargs['text'] == 'Installing plugin...' - assert mock_spinner.mock_calls[2] == call().__enter__().succeed( - 'Installed plugin: [plugin-name-here] [forge-plugin-name-here] [python-version]!' - ) + + mock_spinner.assert_has_calls([ + call().__enter__().succeed( + 'Installed plugin: [plugin-name-here] [forge-plugin-name-here] [python-version]!' + ) + ]) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_install_to_pipx_warn_plugin_already_installed(mock_run_command, mock_spinner): mock_run_command.return_value = ('already seems to be installed', '') + with pytest.raises(PluginManagementWarnException): pipx_wrapper.install_to_pipx('forge-plugin-name', []) - assert mock_spinner.mock_calls[2] == call().__enter__().warn('Plugin already installed!') + mock_spinner.assert_has_calls([ + call().__enter__().warn('Plugin already installed!') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_install_pipx_fail_no_install_message_matched(mock_run_command, mock_spinner): mock_run_command.return_value = ('', '') + with pytest.raises(PluginManagementFatalException): pipx_wrapper.install_to_pipx('forge-plugin-name', []) - assert mock_spinner.mock_calls[2] == call().__enter__().fail('Something went wrong!\nFailed to find package information install log!') + mock_spinner.assert_has_calls([ + call().__enter__().fail('Something went wrong!\nFailed to find package information install log!') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_uninstall_from_pipx(mock_run_command, mock_spinner): mock_run_command.return_value = ('', '') + pipx_wrapper.uninstall_from_pipx('forge-plugin-name', []) mock_run_command.assert_called_once_with(['pipx', 'uninstall', 'forge-plugin-name', '--verbose']) assert mock_spinner.mock_calls[0].kwargs['text'] == 'Uninstalling plugin: [forge-plugin-name]...' - assert mock_spinner.mock_calls[2] == call().__enter__().succeed('Uninstalled plugin: [forge-plugin-name]!') + + mock_spinner.assert_has_calls([ + call().__enter__().succeed('Uninstalled plugin: [forge-plugin-name]!') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.run_command') def test_uninstall_from_pipx_warn_plugin_not_installed(mock_run_command, mock_spinner): mock_run_command.return_value = ('Nothing to uninstall for forge-plugin-name', '') + with pytest.raises(PluginManagementWarnException): pipx_wrapper.uninstall_from_pipx('forge-plugin-name', []) - assert mock_spinner.mock_calls[2] == call().__enter__().warn('Plugin forge-plugin-name not installed!') + mock_spinner.assert_has_calls([ + call().__enter__().warn('Plugin forge-plugin-name not installed!') + ], any_order=True) -@patch('forge.pipx_wrapper.Halo') -@patch('forge.pipx_wrapper.Popen') def test_uninstall_from_pipx_fail_some_pipx_exception(mock_popen, mock_spinner): mock_popen.side_effect = run_command_fail + with pytest.raises(PluginManagementFatalException): pipx_wrapper.uninstall_from_pipx('forge-plugin-name', []) - assert mock_spinner.mock_calls[2] == call().__enter__().fail( - "Something went wrong!\nCommand 'pipx uninstall forge-plugin-name --verbose' returned non-zero exit status 1." - ) + mock_spinner.assert_has_calls([ + call().__enter__().fail( + "Something went wrong!\nCommand 'pipx uninstall forge-plugin-name --verbose' returned non-zero exit status 1." + ) + ], any_order=True) @pytest.mark.parametrize("error_message,expected_result", [ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..470dfdd --- /dev/null +++ b/tox.ini @@ -0,0 +1,47 @@ +[tox] +envlist = + lint + test + bandit + type-check + safety +skipsdist = true + +[testenv] +skip_install = false +parallel_show_output = true + +[testenv:lint] +deps = + -rrequirements.txt + -rdev-requirements.txt +commands = + pylint -j 4 --rcfile=.pylintrc -r y --ignore-patterns=_test.py forge + +[testenv:test] +deps = + -rrequirements.txt + -rdev-requirements.txt +commands = + pytest {posargs:-rf -vvv -x --cov=forge --cov-fail-under=80 --cov-report term-missing --count 10} + +[testenv:bandit] +deps = + bandit==1.7.0 + -rrequirements.txt +commands = + bandit -r forge -s B404,B603 + +[testenv:type-check] +deps = + mypy==0.812 + -rrequirements.txt +commands = + mypy forge + + +[testenv:safety] +deps = + safety==1.10.3 +commands = + safety check -r requirements.txt -r dev-requirements.txt From f3214e906f2028976ccd4bc2bfce9ff95c7dc760 Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Fri, 26 Feb 2021 17:31:40 -0500 Subject: [PATCH 2/7] fix tox errors in gh actions --- .github/workflows/pythonpackage.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 46f0306..719f591 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -39,4 +39,4 @@ jobs: - name: Install tox run: pip install -U tox - name: Lint, Test, Type Check, Bandit, and Safety - run: tox -e lint,test,bandit,type-check,safety -p + run: TOX_PARALLEL_NO_SPINNER=1 tox -e lint,test,bandit,type-check,safety -p diff --git a/tox.ini b/tox.ini index 470dfdd..4bdb761 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = bandit type-check safety -skipsdist = true +skipsdist = false [testenv] skip_install = false From 88de1a937179c91284190c053e13b4f91ab9aeaa Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Fri, 26 Feb 2021 17:35:47 -0500 Subject: [PATCH 3/7] revert changes --- .github/workflows/pythonpackage.yml | 5 +++-- tox.ini | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 719f591..5b0a116 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -10,7 +10,8 @@ on: jobs: build: - + env: + TOX_PARALLEL_NO_SPINNER: 1 runs-on: ${{ matrix.os }} strategy: matrix: @@ -39,4 +40,4 @@ jobs: - name: Install tox run: pip install -U tox - name: Lint, Test, Type Check, Bandit, and Safety - run: TOX_PARALLEL_NO_SPINNER=1 tox -e lint,test,bandit,type-check,safety -p + run: tox -e lint,test,bandit,type-check,safety -p diff --git a/tox.ini b/tox.ini index 4bdb761..217b1cf 100644 --- a/tox.ini +++ b/tox.ini @@ -5,10 +5,9 @@ envlist = bandit type-check safety -skipsdist = false +skipsdist = true [testenv] -skip_install = false parallel_show_output = true [testenv:lint] From e3514220421495e227257032e88234752e541a23 Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Fri, 26 Feb 2021 17:41:45 -0500 Subject: [PATCH 4/7] add usedevelop tox option --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 217b1cf..f55e41a 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ skipsdist = true [testenv] parallel_show_output = true +usedevelop = True [testenv:lint] deps = From cbfdf38631a842abfe027723a8287e0dfd348f13 Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Fri, 26 Feb 2021 18:01:03 -0500 Subject: [PATCH 5/7] fix mypy command --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 96ff747..193b5c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ ignore_missing_imports = True disallow_untyped_calls = True disallow_untyped_defs = True -disallow-incomplete-defs = True +disallow_incomplete_defs = True check_untyped_defs = True no_implicit_optional = True strict_optional = True From e3e016d23952f033c124a7956ac2b7cbd91d9100 Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Fri, 26 Feb 2021 18:06:00 -0500 Subject: [PATCH 6/7] try removing parallel flag to make mac pipeline faster --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5b0a116..ea77fb6 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -40,4 +40,4 @@ jobs: - name: Install tox run: pip install -U tox - name: Lint, Test, Type Check, Bandit, and Safety - run: tox -e lint,test,bandit,type-check,safety -p + run: tox -e lint,test,bandit,type-check,safety From 51a39ddcfc29eadbc42829332f74ee18d768c600 Mon Sep 17 00:00:00 2001 From: Morgan Szafranski Date: Tue, 2 Mar 2021 12:53:57 -0500 Subject: [PATCH 7/7] Add mac install details to README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 1f43734..365a487 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,23 @@ for anyone to create new plugins accessible ### **Unix** +#### Dependencies + ```shell +# MacOS +$ brew install python3-pip +$ brew install python3-venv +$ brew install git + +# Other Linux distros $ apt-get -y install python3-pip $ apt-get -y install python3-venv $ apt-get -y install git +``` + +#### Install PIPX + +```shell $ python3 -m pip install --user pipx $ echo 'export PIPX_HOME="$HOME/.forge"' >> ~/.profile $ echo 'export PIPX_BIN_DIR="$HOME/.forge/bin"' >> ~/.profile