diff --git a/.travis.yml b/.travis.yml index 4e9b02c..45e4c89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - - 3.3 - - 3.4 - - 3.5 + - 3.6 cache: pip: true @@ -15,7 +13,7 @@ env: - PIPENV_NOSPIN=true before_install: - - pip install pipenv + - make setup - make doctor install: diff --git a/.verchew.ini b/.verchew.ini index 58a9c1c..8dd0595 100644 --- a/.verchew.ini +++ b/.verchew.ini @@ -9,7 +9,7 @@ version = GNU Make cli = python -version = Python 3.5. +version = Python 3.6. [pipenv] diff --git a/CHANGELOG.md b/CHANGELOG.md index e12f49e..80c7220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Revision History +## 1.4 (2017/04/18) + +- Added color to display the state of running applications. +- Dropped support for Python 3.3, 3.4, and 3.5. + ## 1.3 (2017/03/13) - Ignored conflicting program name ("iTunes Helper.app"). diff --git a/Makefile b/Makefile index 32707d5..a6ce2e0 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ MODULES := $(wildcard $(PACKAGE)/*.py) # Python settings ifndef TRAVIS PYTHON_MAJOR ?= 3 - PYTHON_MINOR ?= 5 + PYTHON_MINOR ?= 6 endif # System paths @@ -74,8 +74,7 @@ run: install .PHONY: setup setup: - pip install pipenv==3.5.0 - pipenv lock + pip install pipenv==3.5.6 touch Pipfile .PHONY: doctor @@ -104,7 +103,7 @@ else ifdef LINUX endif @ touch $@ -$(METADATA): $(PIP) +$(METADATA): $(PIP) setup.py $(PYTHON) setup.py develop @ touch $@ @@ -150,7 +149,7 @@ PYTEST_OPTIONS := $(PYTEST_CORE_OPTIONS) $(PYTEST_RANDOM_OPTIONS) ifndef DISABLE_COVERAGE PYTEST_OPTIONS += $(PYTEST_COV_OPTIONS) endif -PYTEST_RURUN_OPTIONS := $(PYTEST_CORE_OPTIONS) --last-failed --exitfirst +PYTEST_RERUN_OPTIONS := $(PYTEST_CORE_OPTIONS) --last-failed --exitfirst .PHONY: test test: test-all ## Run unit and integration tests @@ -164,14 +163,14 @@ test-unit: install .PHONY: test-int test-int: install - @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTIONS_FAILFAST) tests; fi + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_RERUN_OPTIONS) tests; fi @ rm -rf $(FAILURES) $(PYTEST) $(PYTEST_OPTIONS) tests --junitxml=$(REPORTS)/integration.xml $(COVERAGE_SPACE) $(REPOSITORY) integration .PHONY: test-all test-all: install - @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTIONS_FAILFAST) $(PACKAGES); fi + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_RERUN_OPTIONS) $(PACKAGES); fi @ rm -rf $(FAILURES) $(PYTEST) $(PYTEST_OPTIONS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml $(COVERAGE_SPACE) $(REPOSITORY) overall diff --git a/Pipfile b/Pipfile index 8d92fb5..8a4e81c 100644 --- a/Pipfile +++ b/Pipfile @@ -6,15 +6,16 @@ verify_ssl = true testpackage = "*" [dev-packages] +pylint = "~=1.7.0" pycodestyle = "*" pydocstyle = "*" -pytest = "*" -pytest-describe = "*" -pytest-expecter = "*" +pytest = "~=3.0.7" +pytest-describe = "==0.11.0" +pytest-expecter = "==0.2.2.post6" pytest-cov = "*" pytest-random = "*" -coverage = "*" -"coverage.space" = ">=0.7.2" +coverage = "~=4.0" +"coverage.space" = "~=0.8" mkdocs = "*" docutils = "*" wheel = "*" @@ -22,11 +23,3 @@ twine = "*" sniffer = "*" Pygments = "*" PyInstaller = "*" - -[dev-packages.pylint] -git = "https://github.com/PyCQA/pylint.git" -ref = "e0fdd25c214e60bef10fbaa46252f4aaa74de8c2" - -[dev-packages.astroid] -git = "https://github.com/PyCQA/astroid.git" -ref = "4e7d9fee4080d2e0db67a3e0463be8b196e56a95" diff --git a/Pipfile.lock b/Pipfile.lock index c111b08..284e128 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cbebbcb93442745b5e9706824e8b35c87c9348fff8b7095e609c1afa8a20f7f4" + "sha256": "03884222f01cac81ef2d5fc7126bcd0c7eaf6f31b79d74dbd001fd58017d8064" }, "requires": {}, "sources": [ @@ -25,8 +25,8 @@ "version": "==2.2.0" }, "setuptools": { - "hash": "sha256:6483f8412313ec787fa71379147a4605d3b1cc303c3648d02542a9160d3db72b", - "version": "==34.3.2" + "hash": "sha256:b427014a0cf196e57727e141899c20381051233094783aa6274780a100eb65d9", + "version": "==35.0.0" }, "six": { "hash": "sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1", @@ -39,8 +39,8 @@ }, "develop": { "Jinja2": { - "hash": "sha256:a7b7438120dbe76a8e735ef7eba6048eaf4e0b7dbc530e100812f8ec462a4d50", - "version": "==2.9.5" + "hash": "sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054", + "version": "==2.9.6" }, "Markdown": { "hash": "sha256:0ac8a81e658167da95d063a9279c9c1b2699f37c7c4153256a458b3a43860e33", @@ -71,8 +71,8 @@ "version": "==0.1.0" }, "astroid": { - "git": "https://github.com/PyCQA/astroid.git", - "ref": "4e7d9fee4080d2e0db67a3e0463be8b196e56a95" + "hash": "sha256:9c19f72d865a539e91115cdb6a293b5c2fecb5f611fad38b21a961a925c2e3f1", + "version": "==1.5.2" }, "backports.shutil_get_terminal_size": { "hash": "sha256:0975ba55054c15e346944b38956a4c9cbee9009391e41b86c68990effb8c1f64", @@ -91,12 +91,12 @@ "version": "==0.3.7" }, "coverage": { - "hash": "sha256:57c0c217270e628380f4befbbf8c5312b88ba7d81fd3d1b2218a25a2608f603c", + "hash": "sha256:8b282292973a1dc4eccfcc0776e0fde75b5b3de2e35164c2d854f7dd80149e4b", "version": "==4.3.4" }, "coverage.space": { - "hash": "sha256:607edd10808e8e9faa52e8e04cc9ad81772ce78aa3ab5ea53d4758df32c32974", - "version": "==0.7.2.post4" + "hash": "sha256:54f0e5bf3cfec621b5625969351207f937668bea9159a4c635f722c997bfe546", + "version": "==0.8" }, "docopt": { "hash": "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", @@ -106,13 +106,25 @@ "hash": "sha256:cb3ebcb09242804f84bdbf0b26504077a054da6772c6f4d625f335cc53ebf94d", "version": "==0.13.1" }, + "isort": { + "hash": "sha256:a0c38c1c5e4c70ea1141daa80c740372ad94351e92e9e07da1cd86ce65653dc4", + "version": "==4.2.5" + }, + "lazy-object-proxy": { + "hash": "sha256:ddd4cf1c74279c349cb7b9c54a2efa5105854f57de5f2d35829ee93631564268", + "version": "==1.2.2" + }, "livereload": { "hash": "sha256:422de10d7ea9467a1ba27cbaffa84c74b809d96fb1598d9de4b9b676adf35e2c", "version": "==2.5.1" }, + "mccabe": { + "hash": "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "version": "==0.6.1" + }, "mkdocs": { - "hash": "sha256:eb3d2a20d7a48b3275a2308baa8827d9d681c051ce5dc6745171986dcd128880", - "version": "==0.16.1" + "hash": "sha256:005b87441084aacd67ada15843c4ed2746bf324103a5bd94c02d36d9b307310f", + "version": "==0.16.3" }, "nose": { "hash": "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", @@ -127,8 +139,8 @@ "version": "==1.4.1" }, "py": { - "hash": "sha256:2d4bba2e25fff58140e6bdce1e485e89bb59776adbe01d490baa6b1f37a3dd6b", - "version": "==1.4.32" + "hash": "sha256:81b5e37db3cc1052de438375605fb5d3b3e97f950f415f9143f04697c684d7eb", + "version": "==1.4.33" }, "pycodestyle": { "hash": "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", @@ -139,16 +151,16 @@ "version": "==1.1.1" }, "pylint": { - "git": "https://github.com/PyCQA/pylint.git", - "ref": "e0fdd25c214e60bef10fbaa46252f4aaa74de8c2" + "hash": "sha256:402931e0b15ac2fa05387a125cb049aba4fcd841c362613c6ad1675e0e5135d8", + "version": "==1.7.1" }, "pyparsing": { "hash": "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010", "version": "==2.2.0" }, "pytest": { - "hash": "sha256:da0ab50c7eec0683bc24f1c1137db1f4111752054ecdad63125e7ec71316b813", - "version": "==3.0.6" + "hash": "sha256:66f332ae62593b874a648b10a8cb106bfdacd2c6288ed7dec3713c3a808a6017", + "version": "==3.0.7" }, "pytest-cov": { "hash": "sha256:10e37e876f49ddec80d6c83a54b657157f1387ebc0f7755285f8c156130014a1", @@ -159,8 +171,8 @@ "version": "==0.11.0" }, "pytest-expecter": { - "hash": "sha256:9c6ed4d26df0e80a31b2c6ad86a959593f0c5b2459ea366496c45d9bbd9d358a", - "version": "==0.2.2.post3" + "hash": "sha256:afa9ee0ece1715d25019cdb8221e57db14118c1551da17724a4e3ee895f8c1aa", + "version": "==0.2.2.post6" }, "pytest-random": { "hash": "sha256:92f25db8c5d9ffc20d90b51997b914372d6955cb9cf1f6ead45b90514fc0eddd", @@ -179,8 +191,8 @@ "version": "==0.7.1" }, "setuptools": { - "hash": "sha256:6483f8412313ec787fa71379147a4605d3b1cc303c3648d02542a9160d3db72b", - "version": "==34.3.2" + "hash": "sha256:b427014a0cf196e57727e141899c20381051233094783aa6274780a100eb65d9", + "version": "==35.0.0" }, "six": { "hash": "sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1", @@ -191,8 +203,8 @@ "version": "==0.4.0" }, "tornado": { - "hash": "sha256:2898f992f898cd41eeb8d53b6df75495f2f423b6672890aadaf196ea1448edcc", - "version": "==4.4.2" + "hash": "sha256:8861cce3c081557cfca2623507290ed647978ea275c29e2e3dfeeb63ca61e855", + "version": "==4.5" }, "twine": { "hash": "sha256:3202d943a144962a821d9c5e92e07f9442dbbe6d6f18eae74e2a725b9980c559", @@ -201,6 +213,10 @@ "wheel": { "hash": "sha256:ea8033fc9905804e652f75474d33410a07404c1a78dd3c949a66863bd1050ebd", "version": "==0.29.0" + }, + "wrapt": { + "hash": "sha256:42160c91b77f1bc64a955890038e02f2f72986c01d462d53cb6cb039b995cdd9", + "version": "==1.10.10" } } } \ No newline at end of file diff --git a/README.md b/README.md index 7f6d4b4..8d605b2 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ and some just don't make sense to keep running on all your computers: ## Requirements -* Python 3.3+ +* Python 3.6+ ## Installation diff --git a/mine/__init__.py b/mine/__init__.py index bc27e2b..c5385e8 100644 --- a/mine/__init__.py +++ b/mine/__init__.py @@ -1,7 +1,7 @@ """Package for mine.""" __project__ = 'mine' -__version__ = '1.3' +__version__ = '1.4' CLI = 'mine' VERSION = '{0} v{1}'.format(__project__, __version__) diff --git a/mine/cli.py b/mine/cli.py index 4daa7e3..7ca8e00 100644 --- a/mine/cli.py +++ b/mine/cli.py @@ -140,8 +140,6 @@ def run(path=None, cleanup=True, delay=None, return manager.launch(path) if delete: return services.delete_conflicts(root, force=force) - if log.getEffectiveLevel() >= logging.WARNING: - print("Updating application state...") if switch is True: switch = computer diff --git a/mine/manager.py b/mine/manager.py index 4ae7f74..977be9e 100644 --- a/mine/manager.py +++ b/mine/manager.py @@ -28,11 +28,11 @@ def wrapped(self, application): log.debug("Determining if %s is running...", application) running = func(self, application) if running is None: - status = "Untracked" + status = "Application untracked" elif running: - status = "Running" + status = "Application running on current machine" else: - status = "Not running" + status = "Application not running on current machine" log.info("%s: %s", status, application) return running return wrapped diff --git a/mine/models/application.py b/mine/models/application.py index c277c7f..d5b0969 100644 --- a/mine/models/application.py +++ b/mine/models/application.py @@ -29,7 +29,8 @@ class Properties(yorm.types.AttributeDictionary): class Application(NameMixin, yorm.types.AttributeDictionary): """Dictionary of application information.""" - def __init__(self, name, properties=None, filename=None, versions=None): + def __init__(self, name=None, properties=None, versions=None, + filename=None): super().__init__() self.name = name self.properties = properties or Properties() diff --git a/mine/models/computer.py b/mine/models/computer.py index c480f35..ee8e659 100644 --- a/mine/models/computer.py +++ b/mine/models/computer.py @@ -18,7 +18,7 @@ class Computer(NameMixin, yorm.types.AttributeDictionary): """A dictionary of identifying computer information.""" - def __init__(self, name, hostname=None, address=None): + def __init__(self, name=None, hostname=None, address=None): super().__init__() self.name = name self.address = address or self.get_address() diff --git a/mine/models/data.py b/mine/models/data.py index b1e8f77..4722dcd 100644 --- a/mine/models/data.py +++ b/mine/models/data.py @@ -3,6 +3,7 @@ import logging import yorm +import crayons from .config import ProgramConfig from .status import ProgramStatus @@ -60,7 +61,8 @@ def launch_queued_applications(config, status, computer, manager): for app_status in status.applications: if app_status.next: application = config.applications.get(app_status.application) - show_queued(application, app_status.next) + print(crayons.yellow( + f"{application} is queued for {app_status.next}")) if app_status.next == computer: latest = status.get_latest(application) if latest in (computer, None) or application.no_wait: @@ -68,7 +70,8 @@ def launch_queued_applications(config, status, computer, manager): manager.start(application) app_status.next = None else: - show_waiting(application, latest) + print(crayons.yellow( + f"{application} is still running on {latest}")) elif manager.is_running(application): manager.stop(application) @@ -84,52 +87,37 @@ def update_status(config, status, computer, manager): """Update each application's status.""" log.info("Recording application status...") for application in config.applications: + latest = status.get_latest(application) if manager.is_running(application): - latest = status.get_latest(application) if computer != latest: if status.is_running(application, computer): # case 1: application just launched remotely manager.stop(application) status.stop(application, computer) - show_running(application, latest) - show_stopped(application, computer) + print(crayons.green( + f"{application} is now running on {latest}")) + print(crayons.red( + f"{application} is now stopped on {computer}")) else: # case 2: application just launched locally status.start(application, computer) - show_running(application, computer) + print(crayons.green( + f"{application} is now running on {computer}")) else: # case 3: application already running locally - pass + print(crayons.magenta( + f"{application} is running on {computer}")) else: if status.is_running(application, computer): # case 4: application just closed locally status.stop(application, computer) - show_stopped(application, computer) - else: + print(crayons.red( + f"{application} is now stopped on {computer}")) + elif latest: # case 5: application already closed locally - pass - - -def show_queued(application, computer): - """Display the state of a queued application.""" - print("{} is queued for {}".format(application, computer)) - - -def show_waiting(application, computer): - """Display the old state of a running application.""" - print("{} is still running on {}".format(application, computer)) - - -def show_running(application, computer): - """Display the new state of a running application.""" - print("{} is now running on {}".format(application, computer)) - - -def show_started(application, computer): - """Display the new state of a started application.""" - print("{} is now started on {}".format(application, computer)) - - -def show_stopped(application, computer): - """Display the new state of a stopped application.""" - print("{} is now stopped on {}".format(application, computer)) + print(crayons.blue( + f"{application} is running on {latest}")) + else: + # case 6: application is not running + print(crayons.white( + f"{application} is not running")) diff --git a/mine/models/status.py b/mine/models/status.py index a873bab..cb935c8 100644 --- a/mine/models/status.py +++ b/mine/models/status.py @@ -51,7 +51,7 @@ def wrapped(self, application, computer): class State(yorm.types.AttributeDictionary): """Dictionary of computer state.""" - def __init__(self, computer, timestamp=None): + def __init__(self, computer=None, timestamp=None): super().__init__() self.computer = computer self.timestamp = timestamp or Timestamp() @@ -74,7 +74,7 @@ class StateList(yorm.types.SortedList): class Status(yorm.types.AttributeDictionary): """Dictionary of computers using an application.""" - def __init__(self, application, computers=None, next=None): # pylint: disable=redefined-builtin + def __init__(self, application=None, computers=None, next=None): # pylint: disable=redefined-builtin super().__init__() self.application = application self.computers = computers or StateList() diff --git a/mine/services.py b/mine/services.py index 2ee991e..dfc99a4 100755 --- a/mine/services.py +++ b/mine/services.py @@ -88,9 +88,7 @@ def delete_conflicts(root=None, config_only=False, force=False): print(path) if count and not force: - print() - print("Run again with '--force' to delete these " - "{} conflict(s)".format(count)) + print(f"\nRun again with '--force' to delete these {count} conflict(s)") return False return True diff --git a/mine/tests/conftest.py b/mine/tests/conftest.py index c444379..64e0970 100644 --- a/mine/tests/conftest.py +++ b/mine/tests/conftest.py @@ -43,10 +43,3 @@ def pytest_runtest_setup(item): pytest.skip("test can only be run on OS X") if 'windows_only' in item.keywords and platform.system() != 'Windows': pytest.skip("test can only be run on Windows") - - -@pytest.fixture -def path(tmpdir): - """Figure to create a temporary settings file path.""" - tmpdir.chdir() - return tmpdir.join('custom.ext').strpath diff --git a/mine/tests/test_cli.py b/mine/tests/test_cli.py index d16f6e5..21c60eb 100644 --- a/mine/tests/test_cli.py +++ b/mine/tests/test_cli.py @@ -1,4 +1,4 @@ -# pylint: disable=misplaced-comparison-constant,no-self-use +# pylint: disable=misplaced-comparison-constant,no-self-use,redefined-outer-name import os from unittest.mock import Mock, patch @@ -11,6 +11,11 @@ from mine.models import Application +@pytest.fixture +def tmp_path(tmpdir): + return tmpdir.join('custom.ext').strpath + + class TestMain: """Unit tests for the `main` function.""" @@ -45,11 +50,11 @@ def test_interrupt_verbose(self, mock_log): assert mock_log.exception.call_count == 1 @patch('mine.cli.daemon', None) - def test_path(self, path): + def test_path(self, tmp_path): """Verify a custom setting file path can be used.""" - cli.main(['--file', path]) + cli.main(['--file', tmp_path]) - assert os.path.isfile(path) + assert os.path.isfile(tmp_path) @patch('mine.cli.run') def test_daemon(self, mock_run): @@ -62,9 +67,9 @@ def test_daemon_with_specific_delay(self, mock_run): mock_run.assert_called_once_with(path=None, delay=42) @patch('mine.cli.daemon', Application(None)) - def test_warning_when_daemon_is_not_running(self, path): + def test_warning_when_daemon_is_not_running(self, tmp_path): with pytest.raises(SystemExit): - cli.main(['--file', path]) + cli.main(['--file', tmp_path]) class TestSwitch: diff --git a/mine/tests/test_services.py b/mine/tests/test_services.py index 3ad9c67..78d12da 100644 --- a/mine/tests/test_services.py +++ b/mine/tests/test_services.py @@ -1,4 +1,4 @@ -# pylint: disable=misplaced-comparison-constant,no-self-use +# pylint: disable=misplaced-comparison-constant,no-self-use,redefined-outer-name import os from unittest.mock import patch, Mock @@ -10,6 +10,23 @@ from mine.tests.conftest import FILES +@pytest.yield_fixture +def tmp_dir(tmpdir): + cwd = os.getcwd() + tmpdir.chdir() + yield str(tmpdir) + os.chdir(cwd) + + +def touch(*parts): + """Create an empty file at the given path.""" + path = os.path.join(*parts) + dirpath = os.path.dirname(path) + if not os.path.isdir(dirpath): + os.makedirs(dirpath) + open(path, 'w').close() + + class TestFindRoot: @patch('os.listdir', Mock(return_value=[])) @@ -27,32 +44,29 @@ def test_ci_workaround_disabled(self): class TestFindConfigPath: - def test_find_dropbox(self, tmpdir): + def test_find_dropbox(self, tmp_dir): """Verify a settings file can be found in Dropbox.""" - tmpdir.chdir() - _touch('Dropbox', 'mine.yml') + touch('Dropbox', 'mine.yml') - path = services.find_config_path(tmpdir.strpath) + path = services.find_config_path(tmp_dir) assert os.path.isfile(path) - def test_find_dropbox_personal(self, tmpdir): + def test_find_dropbox_personal(self, tmp_dir): """Verify a settings file can be found in Dropbox (Personal).""" - tmpdir.chdir() - _touch('Dropbox (Personal)', 'mine.yml') + touch('Dropbox (Personal)', 'mine.yml') - path = services.find_config_path(tmpdir.strpath) + path = services.find_config_path(tmp_dir) assert os.path.isfile(path) @patch('mine.services.DEPTH', 2) - def test_find_depth(self, tmpdir): + def test_find_depth(self, tmp_dir): """Verify a settings file is not found below the maximum depth.""" - tmpdir.chdir() - _touch('Dropbox', 'a', 'b', 'mine.yml') + touch('Dropbox', 'a', 'b', 'mine.yml') with pytest.raises(OSError): - services.find_config_path(tmpdir.strpath) + services.find_config_path(tmp_dir) def test_find_no_share(self): """Verify an error occurs when no service directory is found.""" @@ -65,16 +79,13 @@ def test_find_no_share(self): class TestDeleteConflicts: @staticmethod - def _create_conflicts(tmpdir, count=2): - tmpdir.chdir() - root = str(tmpdir) - + def _create_conflicts(tmp_dir, count=2): for index in range(count): fmt = "{} (Jace's conflicted copy 2015-03-11).fake" filename = fmt.format(index) - _touch(root, filename) + touch(tmp_dir, filename) - return root + return tmp_dir def test_fail_when_leftover_conflicts(self, _, tmpdir): root = self._create_conflicts(tmpdir) @@ -103,12 +114,3 @@ def test_deletion_count_is_correct(self, mock_remove, tmpdir): services.delete_conflicts(root, force=True) assert 2 == mock_remove.call_count - - -def _touch(*parts): - """Create an empty file at the given path.""" - path = os.path.join(*parts) - dirpath = os.path.dirname(path) - if not os.path.isdir(dirpath): - os.makedirs(dirpath) - open(path, 'w').close() diff --git a/scent.py b/scent.py index 06582bc..5f5a6fb 100644 --- a/scent.py +++ b/scent.py @@ -1,7 +1,6 @@ """Configuration file for sniffer.""" # pylint: disable=superfluous-parens,bad-continuation -import os import time import subprocess @@ -17,67 +16,69 @@ watch_paths = ["mine", "tests"] -@select_runnable('python') +class Options(object): + group = int(time.time()) # unique per run + show_coverage = False + rerun_args = None + + targets = [ + (('make', 'test-unit', 'DISABLE_COVERAGE=true'), "Unit Tests", True), + (('make', 'test-all'), "Integration Tests", False), + (('make', 'check'), "Static Analysis", True), + (('make', 'doc'), None, True), + ] + + +@select_runnable('run_targets') @file_validator def python_files(filename): - """Match Python source files.""" + return filename.endswith('.py') - return all(( - filename.endswith('.py'), - not os.path.basename(filename).startswith('.'), - )) + +@select_runnable('run_targets') +@file_validator +def html_files(filename): + return filename.split('.')[-1] in ['html', 'css', 'js'] @runnable -def python(*_): +def run_targets(*args): """Run targets for Python.""" + Options.show_coverage = 'coverage' in args - for count, (command, title, retry) in enumerate(( - (('make', 'test-unit', 'DISABLE_COVERAGE=true'), "Unit Tests", True), - (('make', 'test-all'), "Integration Tests", False), - (('make', 'check'), "Static Analysis", True), - (('make', 'doc'), None, True), - (('make', 'dist'), None, True), - ), start=1): - - if not run(command, title, count, retry): - return False + count = 0 + for count, (command, title, retry) in enumerate(Options.targets, start=1): - return True + success = call(command, title, retry) + if not success: + message = "✅ " * (count - 1) + "❌" + show_notification(message, title) + return False -GROUP = int(time.time()) # unique per run + message = "✅ " * count + title = "All Targets" + show_notification(message, title) + show_coverage() -_show_coverage = False -_rerun_args = None + return True -def run(command, title, count, retry): +def call(command, title, retry): """Run a command-line program and display the result.""" - global _rerun_args - - if _rerun_args: - args = _rerun_args - _rerun_args = None - if not run(*args): + if Options.rerun_args: + command, title, retry = Options.rerun_args + Options.rerun_args = None + success = call(command, title, retry) + if not success: return False print("") print("$ %s" % ' '.join(command)) failure = subprocess.call(command) - if failure: - mark = "❌" * count - message = mark + " [FAIL] " + mark - else: - mark = "✅" * count - message = mark + " [PASS] " + mark - show_notification(message, title) - - show_coverage() - if failure and retry: - _rerun_args = command, title, count, retry + Options.rerun_args = command, title, retry return not failure @@ -85,14 +86,12 @@ def run(command, title, count, retry): def show_notification(message, title): """Show a user notification.""" if notify and title: - notify(message, title=title, group=GROUP) + notify(message, title=title, group=Options.group) def show_coverage(): """Launch the coverage report.""" - global _show_coverage - - if _show_coverage: + if Options.show_coverage: subprocess.call(['make', 'read-coverage']) - _show_coverage = False + Options.show_coverage = False diff --git a/setup.py b/setup.py index f7875a7..86e0168 100644 --- a/setup.py +++ b/setup.py @@ -68,16 +68,15 @@ def build_description(): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: System', 'Topic :: System :: Monitoring', 'Topic :: Utilities', ], install_requires=[ - "YORM ~= 1.1", + "YORM ~= 1.4", "psutil ~= 2.1", + "crayons ~= 0.1.2", ] ) diff --git a/tests/test_all.py b/tests/test_all.py index 769572b..e5778b0 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -279,7 +279,7 @@ def test_case_4(self): data = self._fetch_data() assert not data.status.is_running(self.application, self.computer) - def test_case_5(self): + def test_case_6(self): """Verify an already stopped local application is ignored.""" # Arrange