diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..133fb36 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,29 @@ +environment: + global: + RANDOM_SEED: 0 + matrix: + - PYTHON_MAJOR: 2 + PYTHON_MINOR: 7 + - PYTHON_MAJOR: 3 + PYTHON_MINOR: 3 + - PYTHON_MAJOR: 3 + PYTHON_MINOR: 4 + - PYTHON_MAJOR: 3 + PYTHON_MINOR: 5 + +cache: + - env + +install: + # Export build paths + - copy C:\MinGW\bin\mingw32-make.exe C:\MinGW\bin\make.exe + - set PATH=%PATH%;C:\MinGW\bin + - make --version + # Install project dependencies + - make install + +build: off + +test_script: + - make check + - make test diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..b46d51a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +branch = true +omit = + */env/* + */tests/* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..201aadb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +CHANGELOG.md merge=union diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b08c89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Temporary Python files +*.pyc +*.egg-info +__pycache__ +.ipynb_checkpoints + +# Temporary OS files +Icon* + +# Temporary virtual environment files +.cache +/env + +# Temporary server files +.env +*.pid + +# Generated documentation +/docs/gen +/docs/apidocs +/site +/*.html +/*.rst +/docs/*.png + +# Google Drive +*.gdoc +*.gsheet +*.gslides +*.gdraw + +# Testing and coverage results +.cache +.pytest +.coverage +.coverage.* +/htmlcov +/xmlreport +/pyunit.xml +*.tmp + +# Build and release directories +*.spec +/build +/dist + +# Sublime Text +*.sublime-workspace + +# Eclipse +.settings diff --git a/.pep257 b/.pep257 new file mode 100644 index 0000000..e226c3f --- /dev/null +++ b/.pep257 @@ -0,0 +1,13 @@ +[pep257] + +# D211: No blank lines allowed before class docstring +add_select = D211 + +# D100: Missing docstring in public module +# D101: Missing docstring in public class +# D102: Missing docstring in public method +# D103: Missing docstring in public function +# D104: Missing docstring in public package +# D105: Missing docstring in magic method +# D202: No blank lines allowed after function docstring +add_ignore = D100,D101,D102,D103,D104,D105,D202 diff --git a/.pep8rc b/.pep8rc new file mode 100644 index 0000000..7be92bf --- /dev/null +++ b/.pep8rc @@ -0,0 +1,7 @@ +[pep8] + +# E402 module level import not at top of file (checked by PyLint) +# E501: line too long (checked by PyLint) +# E711: comparison to None (used to improve test style) +# E712: comparison to True (used to improve test style) +ignore = E402,E501,E711,E712 diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..d3c3770 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,21 @@ +[MESSAGES CONTROL] + +disable=locally-disabled,fixme,too-few-public-methods,too-many-public-methods,invalid-name,global-statement,too-many-ancestors,missing-docstring + +[FORMAT] + +max-line-length=80 + +ignore-long-lines=^.*((https?:)|(pragma:)|(TODO:)).*$ + +[SIMILARITIES] + +min-similarity-lines=4 + +ignore-imports=yes + +[REPORTS] + +reports=no + +msg-template={msg_id} ({symbol}):{line:3d},{column}: {msg} diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..84e61bf --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,9 @@ +checks: + python: + code_rating: true + duplicate_code: true +tools: + external_code_coverage: true + pylint: + python_version: 2 + config_file: '.pylintrc' diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5f1c2e7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: python +python: + - 2.7 + - 3.3 + - 3.4 + - 3.5 + +cache: + pip: true + directories: + - env + +env: + global: + - RANDOM_SEED=0 + +install: + - make install + +script: + - make check + - make test + +after_success: + - pip install coveralls scrutinizer-ocular + - coveralls + - ocular + +notifications: + email: + on_success: never + on_failure: change diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a64c20 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision History + +## 0.0.0 (YYYY/MM/DD) + + - TBD diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..06ae232 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# For Contributors + +## Setup + +### Requirements + +* Make: + * Windows: https://cygwin.com/install.html + * Mac: https://developer.apple.com/xcode + * Linux: https://www.gnu.org/software/make (likely already installed) +* virtualenv: https://pypi.python.org/pypi/virtualenv#installation +* Pandoc: http://johnmacfarlane.net/pandoc/installing.html +* Graphviz: http://www.graphviz.org/Download.php + +### Installation + +Install project dependencies into a virtual environment: + +```sh +$ make install +``` + +## Development Tasks + +### Testing + +Manually run the tests: + +```sh +$ make test +$ make tests # includes integration tests +``` + +or keep them running on change: + +```sh +$ make watch +``` + +> In order to have OS X notifications, `brew install terminal-notifier`. + +### Documentation + +Build the documentation: + +```sh +$ make doc +``` + +### Static Analysis + +Run linters and static analyzers: + +```sh +$ make pep8 +$ make pep257 +$ make pylint +$ make check # includes all checks +``` + +## Continuous Integration + +The CI server will report overall build status: + +```sh +$ make ci +``` + +## Release Tasks + +Release to PyPI: + +```sh +$ make upload-test # dry run upload to a test server +$ make upload +``` diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9d20c40 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,23 @@ +# License + +**The MIT License (MIT)** + +Copyright © 2016, Jace Browning + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..855deca --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include *.rst *.txt *.md +recursive-include docs *.rst *.txt *.md +graft */files +graft */*/files diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e500d8a --- /dev/null +++ b/Makefile @@ -0,0 +1,325 @@ +# Project settings +PROJECT := verchew +PACKAGE := verchew +REPOSITORY := jacebrowning/verchew +PACKAGES := $(PACKAGE) tests +CONFIG := $(shell ls *.py) +MODULES := $(shell find $(PACKAGES) -name '*.py') $(CONFIG) + +# Python settings +ifndef TRAVIS + PYTHON_MAJOR ?= 2 + PYTHON_MINOR ?= 7 +endif + +# System paths +PLATFORM := $(shell python -c 'import sys; print(sys.platform)') +ifneq ($(findstring win32, $(PLATFORM)), ) + WINDOWS := true + SYS_PYTHON_DIR := C:\\Python$(PYTHON_MAJOR)$(PYTHON_MINOR) + SYS_PYTHON := $(SYS_PYTHON_DIR)\\python.exe + SYS_VIRTUALENV := $(SYS_PYTHON_DIR)\\Scripts\\virtualenv.exe + # https://bugs.launchpad.net/virtualenv/+bug/449537 + export TCL_LIBRARY=$(SYS_PYTHON_DIR)\\tcl\\tcl8.5 +else + ifneq ($(findstring darwin, $(PLATFORM)), ) + MAC := true + else + LINUX := true + endif + SYS_PYTHON := python$(PYTHON_MAJOR) + ifdef PYTHON_MINOR + SYS_PYTHON := $(SYS_PYTHON).$(PYTHON_MINOR) + endif + SYS_VIRTUALENV := virtualenv +endif + +# Virtual environment paths +ENV := env +ifneq ($(findstring win32, $(PLATFORM)), ) + BIN := $(ENV)/Scripts + ACTIVATE := $(BIN)/activate.bat + OPEN := cmd /c start +else + BIN := $(ENV)/bin + ACTIVATE := . $(BIN)/activate + ifneq ($(findstring cygwin, $(PLATFORM)), ) + OPEN := cygstart + else + OPEN := open + endif +endif + +# Virtual environment executables +ifndef TRAVIS + BIN_ := $(BIN)/ +endif +PYTHON := $(BIN_)python +PIP := $(BIN_)pip +EASY_INSTALL := $(BIN_)easy_install +SNIFFER := $(BIN_)sniffer +HONCHO := $(ACTIVATE) && $(BIN_)honcho + +# MAIN TASKS ################################################################### + +.PHONY: all +all: doc + +.PHONY: ci +ci: check test ## Run all tasks that determine CI status + +.PHONY: watch +watch: install .clean-test ## Continuously run all CI tasks when files chanage + $(SNIFFER) + +.PHONY: run ## Start the program +run: install + $(PYTHON) $(PACKAGE)/__main__.py + +# SYSTEM DEPENDENCIES ########################################################## + +.PHONY: doctor +doctor: ## Confirm system dependencies are available + @ echo "Checking Python version:" + @ python --version | tee /dev/stderr | grep -q "2.7." + +# PROJECT DEPENDENCIES ######################################################### + +DEPS_CI := $(ENV)/.install-ci +DEPS_DEV := $(ENV)/.install-dev +DEPS_BASE := $(ENV)/.install-base + +.PHONY: install +install: $(DEPS_CI) $(DEPS_DEV) $(DEPS_BASE) ## Install all project dependencies + +$(DEPS_CI): requirements/ci.txt $(PIP) + $(PIP) install --upgrade -r $< + @ touch $@ # flag to indicate dependencies are installed + +$(DEPS_DEV): requirements/dev.txt $(PIP) + $(PIP) install --upgrade -r $< +ifdef WINDOWS + @ echo "Manually install pywin32: https://sourceforge.net/projects/pywin32/files/pywin32" +else ifdef MAC + $(PIP) install --upgrade pync MacFSEvents +else ifdef LINUX + $(PIP) install --upgrade pyinotify +endif + @ touch $@ # flag to indicate dependencies are installed + +$(DEPS_BASE): setup.py requirements.txt $(PYTHON) + $(PYTHON) setup.py develop + @ touch $@ # flag to indicate dependencies are installed + +$(PIP): $(PYTHON) + $(PYTHON) -m pip install --upgrade pip setuptools + @ touch $@ + +$(PYTHON): + $(SYS_VIRTUALENV) --python $(SYS_PYTHON) $(ENV) + +# CHECKS ####################################################################### + +PEP8 := $(BIN_)pep8 +PEP8RADIUS := $(BIN_)pep8radius +PEP257 := $(BIN_)pep257 +PYLINT := $(BIN_)pylint + +.PHONY: check +check: pep8 pep257 pylint ## Run linters and static analysis + +.PHONY: pep8 +pep8: install ## Check for convention issues + $(PEP8) $(PACKAGES) $(CONFIG) --config=.pep8rc + +.PHONY: pep257 +pep257: install ## Check for docstring issues + $(PEP257) $(PACKAGES) $(CONFIG) + +.PHONY: pylint +pylint: install ## Check for code issues + $(PYLINT) $(PACKAGES) $(CONFIG) --rcfile=.pylintrc + +.PHONY: fix +fix: install + $(PEP8RADIUS) --docformatter --in-place + +# TESTS ######################################################################## + +PYTEST := $(BIN_)py.test +COVERAGE := $(BIN_)coverage +COVERAGE_SPACE := $(BIN_)coverage.space + +RANDOM_SEED ?= $(shell date +%s) + +PYTEST_CORE_OPTS := --doctest-modules -r xXw -vv +PYTEST_COV_OPTS := --cov=$(PACKAGE) --no-cov-on-fail --cov-report=term-missing --cov-report=html +PYTEST_RANDOM_OPTS := --random --random-seed=$(RANDOM_SEED) + +PYTEST_OPTS := $(PYTEST_CORE_OPTS) $(PYTEST_COV_OPTS) $(PYTEST_RANDOM_OPTS) +PYTEST_OPTS_FAILFAST := $(PYTEST_OPTS) --last-failed --exitfirst + +FAILURES := .cache/v/cache/lastfailed +REPORTS ?= xmlreport + +.PHONY: test +test: test-all + +.PHONY: test-unit +test-unit: install ## Run the unit tests + @- mv $(FAILURES) $(FAILURES).bak + $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) --junitxml=$(REPORTS)/unit.xml + @- mv $(FAILURES).bak $(FAILURES) + $(COVERAGE_SPACE) $(REPOSITORY) unit + +.PHONY: test-int +test-int: install ## Run the integration tests + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi + $(PYTEST) $(PYTEST_OPTS) tests --junitxml=$(REPORTS)/integration.xml + $(COVERAGE_SPACE) $(REPOSITORY) integration + +.PHONY: test-all +test-all: install ## Run all the tests + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(PACKAGES); fi + $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml + $(COVERAGE_SPACE) $(REPOSITORY) overall + +.PHONY: read-coverage +read-coverage: + $(OPEN) htmlcov/index.html + +# DOCUMENTATION ################################################################ + +PYREVERSE := $(BIN_)pyreverse +PDOC := $(PYTHON) $(BIN_)pdoc +MKDOCS := $(BIN_)mkdocs + +PDOC_INDEX := docs/apidocs/$(PACKAGE)/index.html +MKDOCS_INDEX := site/index.html + +.PHONY: doc +doc: uml pdoc mkdocs ## Run documentation generators + +.PHONY: uml +uml: install docs/*.png ## Generate UML diagrams for classes and packages +docs/*.png: $(MODULES) + $(PYREVERSE) $(PACKAGE) -p $(PACKAGE) -a 1 -f ALL -o png --ignore tests + - mv -f classes_$(PACKAGE).png docs/classes.png + - mv -f packages_$(PACKAGE).png docs/packages.png + +.PHONY: pdoc +pdoc: install $(PDOC_INDEX) ## Generate API documentaiton with pdoc +$(PDOC_INDEX): $(MODULES) + $(PDOC) --html --overwrite $(PACKAGE) --html-dir docs/apidocs + @ touch $@ + +.PHONY: mkdocs +mkdocs: install $(MKDOCS_INDEX) ## Build the documentation site with mkdocs +$(MKDOCS_INDEX): mkdocs.yml docs/*.md + ln -sf `realpath README.md --relative-to=docs` docs/index.md + ln -sf `realpath CHANGELOG.md --relative-to=docs/about` docs/about/changelog.md + ln -sf `realpath CONTRIBUTING.md --relative-to=docs/about` docs/about/contributing.md + ln -sf `realpath LICENSE.md --relative-to=docs/about` docs/about/license.md + $(MKDOCS) build --clean --strict + +.PHONY: mkdocs-live +mkdocs-live: mkdocs ## Launch and continuously rebuild the mkdocs site + eval "sleep 3; open http://127.0.0.1:8000" & + $(MKDOCS) serve + +# BUILD ######################################################################## + +PYINSTALLER := $(BIN_)pyinstaller +PYINSTALLER_MAKESPEC := $(BIN_)pyi-makespec + +DIST_FILES := dist/*.tar.gz dist/*.whl +EXE_FILES := dist/$(PROJECT).* + +.PHONY: dist +dist: install $(DIST_FILES) +$(DIST_FILES): $(MODULES) README.rst CHANGELOG.rst + rm -f $(DIST_FILES) + $(PYTHON) setup.py check --restructuredtext --strict --metadata + $(PYTHON) setup.py sdist + $(PYTHON) setup.py bdist_wheel + +%.rst: %.md + pandoc -f markdown_github -t rst -o $@ $< + +.PHONY: exe +exe: install $(EXE_FILES) +$(EXE_FILES): $(MODULES) $(PROJECT).spec + # For framework/shared support: https://github.com/yyuu/pyenv/wiki + $(PYINSTALLER) $(PROJECT).spec --noconfirm --clean + +$(PROJECT).spec: + $(PYINSTALLER_MAKESPEC) $(PACKAGE)/__main__.py --onefile --windowed --name=$(PROJECT) + +# RELEASE ###################################################################### + +TWINE := $(BIN_)twine + +.PHONY: register +register: dist ## Register the project on PyPI + @ echo NOTE: your project must be registered manually + @ echo https://github.com/pypa/python-packaging-user-guide/issues/263 + # TODO: switch to twine when the above issue is resolved + # $(TWINE) register dist/*.whl + +.PHONY: upload +upload: .git-no-changes register ## Upload the current version to PyPI + $(TWINE) upload dist/* + $(OPEN) https://pypi.python.org/pypi/$(PROJECT) + +.PHONY: .git-no-changes +.git-no-changes: + @ if git diff --name-only --exit-code; \ + then \ + echo Git working copy is clean...; \ + else \ + echo ERROR: Git working copy is dirty!; \ + echo Commit your changes and try again.; \ + exit -1; \ + fi; + +# CLEANUP ###################################################################### + +.PHONY: clean +clean: .clean-dist .clean-test .clean-doc .clean-build ## Delete all generated and temporary files + +.PHONY: clean-all +clean-all: clean .clean-env .clean-workspace + +.PHONY: .clean-build +.clean-build: + find $(PACKAGES) -name '*.pyc' -delete + find $(PACKAGES) -name '__pycache__' -delete + rm -rf *.egg-info + +.PHONY: .clean-doc +.clean-doc: + rm -rf README.rst docs/apidocs *.html docs/*.png site + +.PHONY: .clean-test +.clean-test: + rm -rf .cache .pytest .coverage htmlcov xmlreport + +.PHONY: .clean-dist +.clean-dist: + rm -rf *.spec dist build + +.PHONY: .clean-env +.clean-env: clean + rm -rf $(ENV) + +.PHONY: .clean-workspace +.clean-workspace: + rm -rf *.sublime-workspace + +# HELP ######################################################################### + +.PHONY: help +help: all + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b16e16 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +Unix: [![Unix Build Status](https://img.shields.io/travis/jacebrowning/verchew/develop.svg)](https://travis-ci.org/jacebrowning/verchew) Windows: [![Windows Build Status](https://img.shields.io/appveyor/ci/jacebrowning/verchew/develop.svg)](https://ci.appveyor.com/project/jacebrowning/verchew)
Metrics: [![Coverage Status](https://img.shields.io/coveralls/jacebrowning/verchew/develop.svg)](https://coveralls.io/r/jacebrowning/verchew) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jacebrowning/verchew.svg)](https://scrutinizer-ci.com/g/jacebrowning/verchew/?branch=develop)
Usage: [![PyPI Version](https://img.shields.io/pypi/v/verchew.svg)](https://pypi.python.org/pypi/verchew) [![PyPI Downloads](https://img.shields.io/pypi/dm/verchew.svg)](https://pypi.python.org/pypi/verchew) + +# Overview + +This is an embeddable Python script to check the versions of your system dependencies. + +# Setup + +## Requirements + +* Python 2.7+ or Python 3.3+ + +## Installation + +Install verchew with pip: + +```sh +$ pip install verchew +``` + +or directly from the source code: + +```sh +$ git clone https://github.com/jacebrowning/verchew.git +$ cd verchew +$ python setup.py install +``` + +# Usage + +After installation, the package can imported: + +```sh +$ python +>>> import verchew +>>> verchew.__version__ +``` diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..0352491 --- /dev/null +++ b/circle.yml @@ -0,0 +1,18 @@ +machine: + python: + version: 3.5.0 + environment: + RANDOM_SEED: 0 + +dependencies: + override: + - make install + cache_directories: + - env + - components + +test: + override: + - make check + - make test-unit REPORTS=${CIRCLE_TEST_REPORTS} + - make test-int REPORTS=${CIRCLE_TEST_REPORTS} diff --git a/docs/about/changelog.md b/docs/about/changelog.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/docs/about/changelog.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/docs/about/contributing.md b/docs/about/contributing.md new file mode 120000 index 0000000..f939e75 --- /dev/null +++ b/docs/about/contributing.md @@ -0,0 +1 @@ +../../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/about/license.md b/docs/about/license.md new file mode 120000 index 0000000..f0608a6 --- /dev/null +++ b/docs/about/license.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..3e3318b --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,13 @@ +site_name: verchew +site_description: Chews through your system dependencies, spitting out incompatible versions. +site_author: Jace Browning +repo_url: https://github.com/jacebrowning/verchew + +theme: readthedocs + +pages: +- Home: index.md +- About: + - Release Notes: about/changelog.md + - Contributing: about/contributing.md + - License: about/license.md diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/requirements/ci.txt b/requirements/ci.txt new file mode 100644 index 0000000..b91fe06 --- /dev/null +++ b/requirements/ci.txt @@ -0,0 +1,15 @@ +# Linters +pep8 +pep257 +pylint + +# Testing +pytest +pytest-describe +pytest-expecter +pytest-cov +pytest-random + +# Coverage +coverage +coverage.space >= 0.6.2 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..857968f --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,21 @@ +# Documentation +pylint +docutils +readme +pdoc +mkdocs +pygments + +# Tooling +pep8radius +sniffer + +# Runner +honcho + +# Build +wheel +pyinstaller + +# Release +twine diff --git a/scent.py b/scent.py new file mode 100644 index 0000000..f881516 --- /dev/null +++ b/scent.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +"""Configuration file for sniffer.""" +# pylint: disable=superfluous-parens,bad-continuation + +import os +import time +import subprocess + +from sniffer.api import select_runnable, file_validator, runnable +try: + from pync import Notifier +except ImportError: + notify = None +else: + notify = Notifier.notify + + +watch_paths = ["verchew", "tests"] + + +@select_runnable('python') +@file_validator +def python_files(filename): + """Match Python source files.""" + + return all(( + filename.endswith('.py'), + not os.path.basename(filename).startswith('.'), + )) + + +@runnable +def python(*_): + """Run targets for Python.""" + + for count, (command, title, retry) in enumerate(( + (('make', 'test-unit', 'CI=true'), "Unit Tests", True), + (('make', 'test-int', 'CI=true'), "Integration Tests", False), + (('make', 'test-all'), "Combined Tests", False), + (('make', 'check'), "Static Analysis", True), + (('make', 'doc'), None, True), + ), start=1): + + if not run(command, title, count, retry): + return False + + return True + + +GROUP = int(time.time()) # unique per run + +_show_coverage = False +_rerun_args = None + + +def run(command, title, count, 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): + 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 + + return not failure + + +def show_notification(message, title): + """Show a user notification.""" + if notify and title: + notify(message, title=title, group=GROUP) + + +def show_coverage(): + """Launch the coverage report.""" + global _show_coverage + + if _show_coverage: + subprocess.call(['make', 'read-coverage']) + + _show_coverage = False diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0903aeb --- /dev/null +++ b/setup.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +"""Setup script for the package.""" + +import os +import sys + +import setuptools + + +PACKAGE_NAME = 'verchew' +MINIMUM_PYTHON_VERSION = 2, 7 + + +def check_python_version(): + """Exit when the Python version is too low.""" + if sys.version_info < MINIMUM_PYTHON_VERSION: + sys.exit("Python {}.{}+ is required.".format(*MINIMUM_PYTHON_VERSION)) + + +def read_package_variable(key): + """Read the value of a variable from the package without importing.""" + module_path = os.path.join(PACKAGE_NAME, '__init__.py') + with open(module_path) as module: + for line in module: + parts = line.strip().split(' ') + if parts and parts[0] == key: + return parts[-1].strip("'") + assert 0, "'{0}' not found in '{1}'".format(key, module_path) + + +def read_descriptions(): + """Build a description for the project from documentation files.""" + try: + readme = open("README.rst").read() + changelog = open("CHANGELOG.rst").read() + except IOError: + return "" + else: + return readme + '\n' + changelog + + +check_python_version() + +setuptools.setup( + name=read_package_variable('__project__'), + version=read_package_variable('__version__'), + + description="System dependency version checker.", + url='https://github.com/jacebrowning/verchew', + author='Jace Browning', + author_email='jacebrowning@gmail.com', + + packages=setuptools.find_packages(), + + entry_points={'console_scripts': [ + 'verchew-cli = verchew.cli:main', + 'verchew-gui = verchew.gui:main', + ]}, + + long_description=read_descriptions(), + license='MIT', + classifiers=[ + # TODO: update this list to match your application: https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 1 - Planning', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + + install_requires=open("requirements.txt").readlines(), +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a916f18 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Integration tests for the package.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..af9ed2e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,3 @@ +"""Integration tests configuration file.""" + +from verchew.tests.conftest import pytest_configure # pylint: disable=unused-import diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..803091a --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,19 @@ +"""Sample integration test module.""" +# pylint: disable=no-self-use,missing-docstring + +import unittest + +from click.testing import CliRunner + +from verchew.cli import main + + +class TestVerchew(unittest.TestCase): + """Sample integration test class.""" + + def test_conversion(self): + runner = CliRunner() + result = runner.invoke(main, ['42']) + + self.assertEqual(result.exit_code, 0) + self.assertEqual(result.output, "12.80165\n") diff --git a/verchew/__init__.py b/verchew/__init__.py new file mode 100644 index 0000000..4317b07 --- /dev/null +++ b/verchew/__init__.py @@ -0,0 +1,6 @@ +"""Package for verchew.""" + +__project__ = 'verchew' +__version__ = '0.0.0' + +VERSION = "{0} v{1}".format(__project__, __version__) diff --git a/verchew/__main__.py b/verchew/__main__.py new file mode 100644 index 0000000..9cdbecd --- /dev/null +++ b/verchew/__main__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +"""Package entry point.""" + + +from verchew.gui import main + + +if __name__ == '__main__': + main() diff --git a/verchew/cli.py b/verchew/cli.py new file mode 100644 index 0000000..27c0330 --- /dev/null +++ b/verchew/cli.py @@ -0,0 +1,23 @@ +import logging + +import click + +from . import utils + + +log = logging.getLogger(__name__) + + +@click.command() +@click.argument('feet') +def main(feet=None): + logging.basicConfig(level=logging.INFO) + + meters = utils.feet_to_meters(feet) + + if meters is not None: + click.echo(meters) + + +if __name__ == '__main__': + main() diff --git a/verchew/gui.py b/verchew/gui.py new file mode 100644 index 0000000..4128f1f --- /dev/null +++ b/verchew/gui.py @@ -0,0 +1,104 @@ +"""A sample module.""" +from Tkinter import * # pylint: disable=wildcard-import,unused-wildcard-import +from ttk import * # pylint: disable=wildcard-import,unused-wildcard-import +import logging + +from . import utils + + +log = logging.getLogger(__name__) + + +class Application(object): + """A sample class.""" + + def __init__(self): + log.info("Starting the application...") + + # Configure the root window + self.root = Tk() + self.root.title("Feet to Meters") + self.root.minsize(400, 150) + + # Initialize elements + self.feet = StringVar() + self.meters = StringVar() + frame = self.init(self.root) + frame.pack(fill=BOTH, expand=1) + + # Start the event loop + self.root.mainloop() + + def init(self, root): + padded = {'padding': 5} + sticky = {'sticky': NSEW} + + # Configure grid + frame = Frame(root, **padded) + frame.rowconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.columnconfigure(0, weight=1) + + def frame_inputs(root): + frame = Frame(root, **padded) + + # Configure grid + frame.rowconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + + # Place widgets + entry = Entry(frame, width=7, textvariable=self.feet) + entry.grid(column=1, row=0) + entry.focus() + label = Label(frame, text="feet") + label.grid(column=2, row=0) + label = Label(frame, text="is equivalent to") + label.grid(column=0, row=1) + label = Label(frame, text="meters") + label.grid(column=2, row=1) + label = Label(frame, textvariable=self.meters) + label.grid(column=1, row=1) + + return frame + + def frame_commands(root): + frame = Frame(root, **padded) + + # Configure grid + frame.rowconfigure(0, weight=1) + frame.columnconfigure(0, weight=1) + + # Place widgets + button = Button(frame, text="Calculate", command=self.calculate) + button.grid(column=0, row=0) + self.root.bind('', self.calculate) + + return frame + + def separator(root): + return Separator(root) + + # Place widgets + frame_inputs(frame).grid(row=0, **sticky) + separator(frame).grid(row=1, padx=10, pady=5, **sticky) + frame_commands(frame).grid(row=2, **sticky) + + return frame + + def calculate(self, event=None): + log.debug("Event: %s", event) + meters = utils.feet_to_meters(self.feet.get()) + if meters is not None: + self.meters.set(meters) + + +def main(): + logging.basicConfig(level=logging.INFO) + Application() + + +if __name__ == '__main__': + main() diff --git a/verchew/tests/__init__.py b/verchew/tests/__init__.py new file mode 100644 index 0000000..2a67748 --- /dev/null +++ b/verchew/tests/__init__.py @@ -0,0 +1 @@ +"""Unit tests for the package.""" diff --git a/verchew/tests/conftest.py b/verchew/tests/conftest.py new file mode 100644 index 0000000..fdf2d76 --- /dev/null +++ b/verchew/tests/conftest.py @@ -0,0 +1,22 @@ +"""Unit tests configuration file.""" + +import logging + + +def pytest_configure(config): + """Disable verbose output when running tests.""" + logging.basicConfig(level=logging.DEBUG) + + terminal = config.pluginmanager.getplugin('terminal') + base = terminal.TerminalReporter + + class QuietReporter(base): + """A py.test reporting that only shows dots when running tests.""" + + def __init__(self, *args, **kwargs): + base.__init__(self, *args, **kwargs) + self.verbosity = 0 + self.showlongtestinfo = False + self.showfspath = False + + terminal.TerminalReporter = QuietReporter diff --git a/verchew/tests/test_utils.py b/verchew/tests/test_utils.py new file mode 100644 index 0000000..27094e4 --- /dev/null +++ b/verchew/tests/test_utils.py @@ -0,0 +1,15 @@ +"""Sample unit test module.""" + +import unittest + +from verchew import utils + + +class TestVerchew(unittest.TestCase): + """Sample unit test class.""" + + def test_conversion_is_correct(self): + self.assertEqual(utils.feet_to_meters(42), 12.80165) + + def test_invalid_inputs_are_handled(self): + self.assertEqual(utils.feet_to_meters("hello"), None) diff --git a/verchew/utils.py b/verchew/utils.py new file mode 100644 index 0000000..37d80b9 --- /dev/null +++ b/verchew/utils.py @@ -0,0 +1,16 @@ +"""A sample module.""" + +import logging + + +log = logging.getLogger(__name__) + + +def feet_to_meters(feet): + """Convert feet to meters.""" + try: + value = float(feet) + except ValueError: + log.error("Unable to convert to float: %s", feet) + else: + return (0.3048 * value * 10000.0 + 0.5) / 10000.0