diff --git a/.coveragerc b/.coveragerc index b46d51a..1fcaece 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,5 @@ [run] branch = true omit = - */env/* + */.venv/* */tests/* diff --git a/.gitignore b/.gitignore index 01d2a5e..62c63c8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,17 +8,17 @@ __pycache__ Icon* # Temporary virtual environment files -/.cache -/env +/.cache/ +/.venv/ # Temporary server files .env *.pid # Generated documentation -/docs/gen -/docs/apidocs -/site +/docs/gen/ +/docs/apidocs/ +/site/ /*.html /*.rst /docs/*.png @@ -30,19 +30,19 @@ Icon* *.gdraw # Testing and coverage results -/.pytest +/.pytest/ /.coverage /.coverage.* -/htmlcov -/xmlreport +/htmlcov/ +/xmlreport/ /pyunit.xml -/tmp +/tmp/ *.tmp # Build and release directories +/build/ +/dist/ *.spec -/build -/dist # Sublime Text *.sublime-workspace diff --git a/.pylint.ini b/.pylint.ini index 8513660..1bbf12f 100644 --- a/.pylint.ini +++ b/.pylint.ini @@ -1,23 +1,410 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + [MESSAGES CONTROL] -disable=locally-disabled,fixme,too-few-public-methods,too-many-public-methods,invalid-name,global-statement,too-many-ancestors,missing-docstring,too-many-arguments,too-many-branches,too-many-instance-attributes +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,import-star-module-level,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,long-suffix,old-ne-operator,old-octal-literal,suppressed-message,useless-suppression,locally-disabled,fixme,too-few-public-methods,too-many-public-methods,invalid-name,global-statement,too-many-ancestors,missing-docstring,no-else-return,too-many-instance-attributes,too-many-branches,arguments-differ + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +msg-template={msg_id} ({symbol}):{line:3d},{column}: {msg} + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + [FORMAT] +# Maximum number of characters on a single line. max-line-length=80 +# Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^.*((https?:)|(pragma:)|(TODO:)).*$ +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + [SIMILARITIES] -min-similarity-lines=10 +# Minimum lines number of a similarity. +min-similarity-lines=4 +# Ignore comments when computing similarities. ignore-comments=yes + +# Ignore docstrings when computing similarities. ignore-docstrings=yes + +# Ignore imports when computing similarities. ignore-imports=yes -[REPORTS] -reports=no +[SPELLING] -msg-template={msg_id} ({symbol}):{line:3d},{column}: {msg} +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.scrutinizer.yml b/.scrutinizer.yml index f097d04..92c7344 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,12 +1,11 @@ +build: + tests: + override: + - pylint-run --rcfile=.pylint.ini checks: python: code_rating: true duplicate_code: true -tools: - external_code_coverage: true - pylint: - python_version: 3 - config_file: '.pylintrc' filter: excluded_paths: - "*/tests/*" diff --git a/.travis.yml b/.travis.yml index 813b3c1..bfb6596 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,17 +3,20 @@ python: - 3.3 - 3.4 - 3.5 + - 3.6 cache: pip: true directories: - - ~/virtualenv/python3.5.* + - .venv env: global: - RANDOM_SEED=0 + - PIPENV_NOSPIN=true before_install: + - pip install pipenv~=4.0.1 - make doctor install: diff --git a/.verchew.ini b/.verchew.ini index 821d4fb..cf1902b 100644 --- a/.verchew.ini +++ b/.verchew.ini @@ -12,6 +12,13 @@ cli = python version = Python 3. +[pipenv] + +cli = pipenv + +version = 4. + + [pandoc] cli = pandoc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6989770..3588cbd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,7 @@ * Windows: http://mingw.org/download/installer * Mac: http://developer.apple.com/xcode * Linux: http://www.gnu.org/software/make +* pipenv: http://docs.pipenv.org * Pandoc: http://johnmacfarlane.net/pandoc/installing.html * Graphviz: http://www.graphviz.org/Download.php @@ -33,7 +34,6 @@ Manually run the tests: ```sh $ make test -$ make tests # includes integration tests ``` or keep them running on change: @@ -57,9 +57,9 @@ $ make doc Run linters and static analyzers: ```sh -$ make pep8 -$ make pep257 $ make pylint +$ make pycodestyle +$ make pydocstyle $ make check # includes all checks ``` @@ -76,6 +76,5 @@ $ make ci Release to PyPI: ```sh -$ make upload-test # dry run upload to a test server $ make upload ``` diff --git a/Makefile b/Makefile index 6998460..2c2e436 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 @@ -35,15 +35,13 @@ else endif # Virtual environment paths -ifdef TRAVIS - ENV := $(shell dirname $(shell dirname $(shell which $(SYS_PYTHON))))/ -else - ENV := env -endif +ENV := .venv ifneq ($(findstring win32, $(PLATFORM)), ) BIN := $(ENV)/Scripts ACTIVATE := $(BIN)/activate.bat OPEN := cmd /c start + PYTHON := $(BIN)/python.exe + PIP := $(BIN)/pip.exe else BIN := $(ENV)/bin ACTIVATE := . $(BIN)/activate @@ -52,17 +50,14 @@ else else OPEN := open endif + PYTHON := $(BIN)/python + PIP := $(BIN)/pip endif -# Virtual environment executables -PYTHON := $(BIN)/python -PIP := $(BIN)/pip -EASY_INSTALL := $(BIN)/easy_install -SNIFFER := $(BIN)/sniffer -HONCHO := $(ACTIVATE) && $(BIN)/honcho - # MAIN TASKS ################################################################### +SNIFFER := pipenv run sniffer + .PHONY: all all: install @@ -85,44 +80,38 @@ doctor: ## Confirm system dependencies are available # PROJECT DEPENDENCIES ######################################################### -DEPS_CI := $(ENV)/.install-ci -DEPS_DEV := $(ENV)/.install-dev -DEPS_BASE := $(ENV)/.install-base +export PIPENV_SHELL_COMPAT=true +export PIPENV_VENV_IN_PROJECT=true -.PHONY: install -install: $(DEPS_CI) $(DEPS_DEV) $(DEPS_BASE) ## Install all project dependencies +DEPENDENCIES := $(ENV)/.installed +METADATA := *.egg-info -$(DEPS_CI): requirements/ci.txt $(PIP) - $(PIP) install --upgrade -r $< - @ touch $@ # flag to indicate dependencies are installed +.PHONY: install +install: $(DEPENDENCIES) $(METADATA) -$(DEPS_DEV): requirements/dev.txt $(PIP) - $(PIP) install --upgrade -r $< +$(DEPENDENCIES): $(PIP) Pipfile* + pipenv install --dev ifdef WINDOWS @ echo "Manually install pywin32: https://sourceforge.net/projects/pywin32/files/pywin32" else ifdef MAC - $(PIP) install --upgrade pync MacFSEvents + $(PIP) install pync MacFSEvents else ifdef LINUX - $(PIP) install --upgrade pyinotify + $(PIP) install pyinotify endif - @ touch $@ # flag to indicate dependencies are installed + @ touch $@ -$(DEPS_BASE): setup.py $(PYTHON) +$(METADATA): $(PIP) setup.py $(PYTHON) setup.py develop - @ touch $@ # flag to indicate dependencies are installed - -$(PIP): $(PYTHON) - $(PYTHON) -m pip install --upgrade pip setuptools @ touch $@ -$(PYTHON): - $(SYS_PYTHON) -m venv $(ENV) +$(PIP): + pipenv --python=$(SYS_PYTHON) # CHECKS ####################################################################### -PYLINT := $(BIN)/pylint -PYCODESTYLE := $(BIN)/pycodestyle -PYDOCSTYLE := $(BIN)/pydocstyle +PYLINT := pipenv run pylint +PYCODESTYLE := pipenv run pycodestyle +PYDOCSTYLE := pipenv run pydocstyle .PHONY: check check: pylint pycodestyle pydocstyle ## Run linters and static analysis @@ -141,44 +130,46 @@ pydocstyle: install # TESTS ######################################################################## -PYTEST := $(BIN)/py.test -COVERAGE := $(BIN)/coverage -COVERAGE_SPACE := $(BIN)/coverage.space +PYTEST := pipenv run py.test +COVERAGE := pipenv run coverage +COVERAGE_SPACE := pipenv run coverage.space RANDOM_SEED ?= $(shell date +%s) - -PYTEST_CORE_OPTS := -ra -vv -PYTEST_COV_OPTS := --cov=$(PACKAGE) --no-cov-on-fail --cov-report=term-missing:skip-covered --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 +PYTEST_CORE_OPTIONS := -ra -vv +PYTEST_COV_OPTIONS := --cov=$(PACKAGE) --no-cov-on-fail --cov-report=term-missing:skip-covered --cov-report=html +PYTEST_RANDOM_OPTIONS := --random --random-seed=$(RANDOM_SEED) + +PYTEST_OPTIONS := $(PYTEST_CORE_OPTIONS) $(PYTEST_RANDOM_OPTIONS) +ifndef DISABLE_COVERAGE +PYTEST_OPTIONS += $(PYTEST_COV_OPTIONS) +endif +PYTEST_RERUN_OPTIONS := $(PYTEST_CORE_OPTIONS) --last-failed --exitfirst + .PHONY: test test: test-all ## Run unit and integration tests .PHONY: test-unit test-unit: install @- mv $(FAILURES) $(FAILURES).bak - $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) --junitxml=$(REPORTS)/unit.xml + $(PYTEST) $(PYTEST_OPTIONS) $(PACKAGE) --junitxml=$(REPORTS)/unit.xml @- mv $(FAILURES).bak $(FAILURES) $(COVERAGE_SPACE) $(REPOSITORY) unit .PHONY: test-int test-int: install - @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_RERUN_OPTIONS) tests; fi @ rm -rf $(FAILURES) - $(PYTEST) $(PYTEST_OPTS) tests --junitxml=$(REPORTS)/integration.xml + $(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_OPTS_FAILFAST) $(PACKAGES); fi + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_RERUN_OPTIONS) $(PACKAGES); fi @ rm -rf $(FAILURES) - $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml + $(PYTEST) $(PYTEST_OPTIONS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml $(COVERAGE_SPACE) $(REPOSITORY) overall .PHONY: read-coverage @@ -187,15 +178,13 @@ read-coverage: # DOCUMENTATION ################################################################ -PYREVERSE := $(BIN)/pyreverse -PDOC := $(PYTHON) $(BIN)/pdoc -MKDOCS := $(BIN)/mkdocs +PYREVERSE := pipenv run pyreverse +MKDOCS := pipenv run mkdocs -PDOC_INDEX := docs/apidocs/$(PACKAGE)/index.html MKDOCS_INDEX := site/index.html .PHONY: doc -doc: uml pdoc mkdocs ## Generate documentaiton +doc: uml mkdocs ## Generate documentation .PHONY: uml uml: install docs/*.png @@ -204,12 +193,6 @@ docs/*.png: $(MODULES) - mv -f classes_$(PACKAGE).png docs/classes.png - mv -f packages_$(PACKAGE).png docs/packages.png -.PHONY: pdoc -pdoc: install $(PDOC_INDEX) -$(PDOC_INDEX): $(MODULES) - $(PDOC) --html --overwrite $(PACKAGE) --html-dir docs/apidocs - @ touch $@ - .PHONY: mkdocs mkdocs: install $(MKDOCS_INDEX) $(MKDOCS_INDEX): mkdocs.yml docs/*.md @@ -226,8 +209,8 @@ mkdocs-live: mkdocs # BUILD ######################################################################## -PYINSTALLER := $(BIN)/pyinstaller -PYINSTALLER_MAKESPEC := $(BIN)/pyi-makespec +PYINSTALLER := pipenv run pyinstaller +PYINSTALLER_MAKESPEC := pipenv run pyi-makespec DIST_FILES := dist/*.tar.gz dist/*.whl EXE_FILES := dist/$(PROJECT).* @@ -254,7 +237,7 @@ $(PROJECT).spec: # RELEASE ###################################################################### -TWINE := $(BIN)/twine +TWINE := pipenv run twine .PHONY: register register: dist ## Register the project on PyPI @@ -265,7 +248,7 @@ register: dist ## Register the project on PyPI .PHONY: upload upload: .git-no-changes register ## Upload the current version to PyPI - $(TWINE) upload dist/* + $(TWINE) upload dist/*.* $(OPEN) https://pypi.python.org/pypi/$(PROJECT) .PHONY: .git-no-changes diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..8a77a3e --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true + +[dev-packages] +pylint = "~=1.7" +pycodestyle = "~=2.3" +pydocstyle = "~=2.0" +pytest = "~=3.0.7" +pytest-describe = "==0.11.0" +pytest-expecter = "==0.2.2.post6" +pytest-cov = "*" +pytest-random = "*" +coverage = "~=4.0" +"coverage.space" = "~=0.8" +mkdocs = "*" +docutils = "*" +wheel = "*" +twine = "*" +sniffer = "*" +Pygments = "*" +PyInstaller = "*" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..cbed986 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,164 @@ +{ + "_meta": { + "hash": { + "sha256": "88d75b88451b68c9fc8b791cb67b08b31ec2d1358e30c0923f357001b252c506" + }, + "requires": {}, + "sources": [ + { + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "appdirs": { + "version": "==1.4.3" + }, + "astroid": { + "version": "==1.5.2" + }, + "backports.shutil-get-terminal-size": { + "version": "==1.0.0" + }, + "certifi": { + "version": "==2017.4.17" + }, + "chardet": { + "version": "==3.0.3" + }, + "click": { + "version": "==6.7" + }, + "colorama": { + "version": "==0.3.9" + }, + "coverage": { + "version": "==4.4.1" + }, + "coverage.space": { + "version": "==0.8" + }, + "docopt": { + "version": "==0.6.2" + }, + "docutils": { + "version": "==0.13.1" + }, + "idna": { + "version": "==2.5" + }, + "isort": { + "version": "==4.2.5" + }, + "jinja2": { + "version": "==2.9.6" + }, + "lazy-object-proxy": { + "version": "==1.3.1" + }, + "livereload": { + "version": "==2.5.1" + }, + "markdown": { + "version": "==2.6.8" + }, + "markupsafe": { + "version": "==1.0" + }, + "mccabe": { + "version": "==0.6.1" + }, + "mkdocs": { + "version": "==0.16.3" + }, + "nose": { + "version": "==1.3.7" + }, + "packaging": { + "version": "==16.8" + }, + "pkginfo": { + "version": "==1.4.1" + }, + "py": { + "version": "==1.4.33" + }, + "pycodestyle": { + "version": "==2.3.1" + }, + "pydocstyle": { + "version": "==2.0.0" + }, + "pygments": { + "version": "==2.2.0" + }, + "pyinstaller": { + "version": "==3.2.1" + }, + "pylint": { + "version": "==1.7.1" + }, + "pyparsing": { + "version": "==2.2.0" + }, + "pytest": { + "version": "==3.1.1" + }, + "pytest-cov": { + "version": "==2.5.1" + }, + "pytest-describe": { + "version": "==0.11.0" + }, + "pytest-expecter": { + "version": "==0.2.2.post6" + }, + "pytest-random": { + "version": "==0.02" + }, + "python-termstyle": { + "version": "==0.1.10" + }, + "pyyaml": { + "version": "==3.12" + }, + "requests": { + "version": "==2.17.3" + }, + "requests-toolbelt": { + "version": "==0.8.0" + }, + "setuptools": { + "version": "==35.0.2" + }, + "six": { + "version": "==1.10.0" + }, + "sniffer": { + "version": "==0.4.0" + }, + "snowballstemmer": { + "version": "==1.2.1" + }, + "tornado": { + "version": "==4.5.1" + }, + "tqdm": { + "version": "==4.14.0" + }, + "twine": { + "version": "==1.9.1" + }, + "urllib3": { + "version": "==1.21.1" + }, + "wheel": { + "version": "==0.29.0" + }, + "wrapt": { + "version": "==1.10.10" + } + } +} diff --git a/README.md b/README.md index d58e65a..e5fb02a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [![Coverage Status](http://img.shields.io/coveralls/jacebrowning/yorm/master.svg)](https://coveralls.io/r/jacebrowning/yorm) [![Scrutinizer Code Quality](http://img.shields.io/scrutinizer/g/jacebrowning/yorm.svg)](https://scrutinizer-ci.com/g/jacebrowning/yorm/?branch=master) [![PyPI Version](http://img.shields.io/pypi/v/yorm.svg)](https://pypi.python.org/pypi/yorm) -[![PyPI Downloads](http://img.shields.io/pypi/dm/yorm.svg)](https://pypi.python.org/pypi/yorm) - # Overview @@ -25,13 +23,13 @@ View the talk from [PyOhio 2015](https://www.youtube.com/watch?v=0woNEKf-wAo). Install YORM with pip: -``` +```sh $ pip install YORM ``` or directly from the source code: -``` +```sh $ git clone https://github.com/jacebrowning/yorm.git $ cd yorm $ python setup.py install @@ -73,7 +71,7 @@ Modifications to each object's mapped attributes: are automatically reflected on the filesytem: -```bash +```sh $ cat students/GVSU/123.yml name: John Doe gpa: 3.0 @@ -83,7 +81,7 @@ year: 2009 Modifications and new content in each mapped file: -```bash +```sh $ echo "name: John Doe > gpa: 1.8 > year: 2010 diff --git a/scent.py b/scent.py index 926225a..f5c96e8 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 = ["yorm", "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', '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 + 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 9fc7276..7551b84 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def read_package_variable(key, filename='__init__.py'): sys.exit("'{0}' not found in '{1}'".format(key, module_path)) -def read_descriptions(): +def build_description(): """Build a description for the project from documentation files.""" try: readme = open("README.rst").read() @@ -55,7 +55,7 @@ def read_descriptions(): entry_points={'console_scripts': []}, - long_description=read_descriptions(), + long_description=build_description(), license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/tests/test_files.py b/tests/test_files.py index 645f091..707f966 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -33,14 +33,17 @@ class SampleStandardDecorated: """Sample class using standard attribute types.""" def __init__(self, name, category='default'): + # pylint: disable=duplicate-code self.name = name self.category = category # https://docs.python.org/3.4/library/json.html#json.JSONDecoder self.object = {} self.array = [] + # pylint: disable=duplicate-code self.string = "" self.number_int = 0 self.number_real = 0.0 + # pylint: disable=duplicate-code self.true = True self.false = False self.null = None diff --git a/tests/test_ordering.py b/tests/test_ordering.py index cfd68e9..b5b316f 100644 --- a/tests/test_ordering.py +++ b/tests/test_ordering.py @@ -30,6 +30,7 @@ def test_attribute_order_is_maintained(tmpdir): sample.string = "Hello, world!" sample.number_int = 42 sample.number_real = 4.2 + # pylint: disable=duplicate-code sample.truthy = False sample.falsey = True sample.dictionary['status'] = 1 diff --git a/yorm/bases/mappable.py b/yorm/bases/mappable.py index 97a25f6..1e00500 100644 --- a/yorm/bases/mappable.py +++ b/yorm/bases/mappable.py @@ -10,14 +10,13 @@ def load_before(method): - """Decorator for methods that should load before call.""" + """Decorate methods that should load before call.""" if getattr(method, '_load_before', False): return method @functools.wraps(method) def wrapped(self, *args, **kwargs): - """Decorated method.""" __tracebackhide__ = True # pylint: disable=unused-variable if not _private_call(method, args): @@ -37,14 +36,13 @@ def wrapped(self, *args, **kwargs): def save_after(method): - """Decorator for methods that should save after call.""" + """Decorate methods that should save after call.""" if getattr(method, '_save_after', False): return method @functools.wraps(method) def wrapped(self, *args, **kwargs): - """Decorated method.""" __tracebackhide__ = True # pylint: disable=unused-variable result = method(self, *args, **kwargs) diff --git a/yorm/common.py b/yorm/common.py index b6f57c1..d1521a9 100644 --- a/yorm/common.py +++ b/yorm/common.py @@ -28,13 +28,15 @@ # LOGGING ###################################################################### -def _trace(self, message, *args, **kwargs): # pragma: no cover (manual test) - """Handler for a new TRACE logging level.""" +logging.addLevelName(logging.DEBUG - 1, 'TRACE') + + +def _trace(self, message, *args, **kwargs): if self.isEnabledFor(logging.DEBUG - 1): - self._log(logging.DEBUG - 1, message, args, **kwargs) # pylint: disable=protected-access + # pylint: disable=protected-access + self._log(logging.DEBUG - 1, message, args, **kwargs) -logging.addLevelName(logging.DEBUG - 1, "TRACE") logging.Logger.trace = _trace diff --git a/yorm/decorators.py b/yorm/decorators.py index 23240de..475433a 100644 --- a/yorm/decorators.py +++ b/yorm/decorators.py @@ -12,7 +12,7 @@ def sync(*args, **kwargs): - """Convenience function to forward calls based on arguments. + """Decorate class or map object based on arguments. This function will call either: @@ -64,7 +64,7 @@ def sync_object(instance, path, attrs=None, **kwargs): def sync_instances(path_format, format_spec=None, attrs=None, **kwargs): - """Class decorator to enable YAML mapping after instantiation. + """Decorate class to enable YAML mapping after instantiation. :param path_format: formatting string to create file paths for dump/parse :param format_spec: dictionary to use for string formatting @@ -83,7 +83,6 @@ def decorator(cls): init = cls.__init__ def modified_init(self, *_args, **_kwargs): - """Modified class __init__ that maps the resulting instance.""" init(self, *_args, **_kwargs) log.info("Mapping instance of %r to '%s'...", cls, path_format) diff --git a/yorm/mapper.py b/yorm/mapper.py index 9305af7..fe6f20e 100644 --- a/yorm/mapper.py +++ b/yorm/mapper.py @@ -11,7 +11,7 @@ def file_required(method): - """Decorator for methods that require the file to exist.""" + """Decorate methods that require the file to exist.""" @functools.wraps(method) def wrapped(self, *args, **kwargs): @@ -28,7 +28,7 @@ def wrapped(self, *args, **kwargs): def prevent_recursion(method): - """Decorator to prevent indirect recursive calls.""" + """Decorate methods to prevent indirect recursive calls.""" @functools.wraps(method) def wrapped(self, *args, **kwargs): diff --git a/yorm/tests/test_bases_container.py b/yorm/tests/test_bases_container.py index b472982..b5bebaf 100644 --- a/yorm/tests/test_bases_container.py +++ b/yorm/tests/test_bases_container.py @@ -23,7 +23,7 @@ def create_default(cls): def to_data(cls, value): return str(value.value) - def update_value(self, data, *, auto_track=None): # pylint: disable=unused-variable + def update_value(self, data, *, auto_track=None): # pylint: disable=unused-argument self.value += int(data) def test_container_class_cannot_be_instantiated(self): diff --git a/yorm/tests/test_decorators.py b/yorm/tests/test_decorators.py index e015ecf..c196d88 100644 --- a/yorm/tests/test_decorators.py +++ b/yorm/tests/test_decorators.py @@ -1,4 +1,4 @@ -# pylint: disable=unused-variable,expression-not-assigned +# pylint: disable=unused-variable,unused-argument,expression-not-assigned # pylint: disable=missing-docstring,no-self-use,no-member,misplaced-comparison-constant import logging @@ -21,11 +21,11 @@ def create_default(cls): return None @classmethod - def to_value(cls, *_): + def to_value(cls, data): return None @classmethod - def to_data(cls, _): + def to_data(cls, value): return None