diff --git a/.appveyor.yml b/.appveyor.yml index f7a98395..32138abf 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -4,16 +4,22 @@ environment: matrix: - PYTHON_MAJOR: 3 PYTHON_MINOR: 5 + - PYTHON_MAJOR: 3 + PYTHON_MINOR: 6 + - PYTHON_MAJOR: 3 + PYTHON_MINOR: 7 cache: - - env + - .venv -> Pipfile.lock install: - # Export build paths + # Add Make and Python to the PATH - copy C:\MinGW\bin\mingw32-make.exe C:\MinGW\bin\make.exe - set PATH=%PATH%;C:\MinGW\bin - - make --version - # Check system dependencies + - set PATH=C:\Python%PYTHON_MAJOR%%PYTHON_MINOR%;%PATH% + - set PATH=C:\Python%PYTHON_MAJOR%%PYTHON_MINOR%\Scripts;%PATH% + # Install system dependencies + - pip install pipenv - make doctor # Install project dependencies - make install diff --git a/.coveragerc b/.coveragerc index b46d51a5..5033e7f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,7 @@ [run] + branch = true + omit = - */env/* + .venv/* */tests/* diff --git a/.gitignore b/.gitignore index 3f9f1e05..cdbb8df1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,19 +12,18 @@ __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 # Google Drive @@ -34,19 +33,14 @@ Icon* *.gdraw # Testing and coverage results -.cache -.pytest -.coverage -.coverage.* -/htmlcov -/xmlreport -/pyunit.xml -*.tmp +/.coverage +/.coverage.* +/htmlcov/ # Build and release directories +/build/ +/dist/ *.spec -/build -/dist # Sublime Text *.sublime-workspace diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..361814db --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,13 @@ +[settings] + +not_skip = __init__.py + +multi_line_output = 5 + +known_third_party = click,log +known_first_party = demo + +combine_as_imports = true +include_trailing_comma = true + +lines_after_imports = 2 diff --git a/.pycodestyle.ini b/.pycodestyle.ini index 85baad55..16c85a65 100644 --- a/.pycodestyle.ini +++ b/.pycodestyle.ini @@ -1,8 +1,9 @@ [pycodestyle] +# W504 line break after binary operator # E401 multiple imports on one line (checked by PyLint) # 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 = E401,E402,E501,E711,E712 +# 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 = W504,E401,E402,E501,E711,E712 diff --git a/.pydocstyle b/.pydocstyle index 23481f04..69e38cb6 100644 --- a/.pydocstyle +++ b/.pydocstyle @@ -9,5 +9,6 @@ add_select = D211 # D103: Missing docstring in public function # D104: Missing docstring in public package # D105: Missing docstring in magic method +# D107: Missing docstring in __init__ # D202: No blank lines allowed after function docstring -add_ignore = D100,D101,D102,D103,D104,D105,D202 +add_ignore = D100,D101,D102,D103,D104,D105,D107,D202 diff --git a/.pylint.ini b/.pylint.ini index 260e4296..e1089c42 100644 --- a/.pylint.ini +++ b/.pylint.ini @@ -1,21 +1,499 @@ +[MASTER] + +# 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= + +# 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= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + [MESSAGES CONTROL] -disable=locally-disabled,fixme,too-few-public-methods,too-many-public-methods,invalid-name,global-statement,too-many-ancestors,missing-docstring,no-member,too-many-arguments +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# 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, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + raw-checker-failed, + bad-inline-option, + locally-disabled, + locally-enabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + 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, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + missing-docstring, + invalid-name, + too-few-public-methods, + fixme, + too-many-arguments, + too-many-branches, + keyword-arg-before-vararg, + logging-not-lazy, + +# 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= + + +[REPORTS] + +# 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= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-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 names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-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 module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# 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 + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + [FORMAT] -max-line-length=80 +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= +# Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^.*((https?:)|(pragma:)|(TODO:)).*$ +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=79 + +# Maximum number of lines in a module +max-module-lines=1000 + +# 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 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# 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 + + +[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] +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. min-similarity-lines=4 -ignore-imports=yes -[REPORTS] +[SPELLING] -reports=no +# 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] + +# 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 + +# 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= + +# 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 + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# 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 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= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# 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 + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# 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 member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# 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 + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# 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 + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# 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 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 + + +[EXCEPTIONS] -msg-template={msg_id} ({symbol}):{line:3d},{column}: {msg} +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.scrutinizer.yml b/.scrutinizer.yml index f097d048..67407d9c 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,12 +1,18 @@ +build: + tests: + override: + - pylint-run --rcfile=.pylint.ini + nodes: + py35: + environment: + python: 3.5.1 + py36: + environment: + python: 3.6 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 eb036cce..03524e7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,24 @@ language: python python: - 3.5 + - 3.6 +matrix: + include: + - python: 3.7 + dist: xenial + sudo: true cache: pip: true directories: - - ~/virtualenv/python3.5.* + - .venv env: global: - RANDOM_SEED=0 before_install: + - pip install pipenv - make doctor install: diff --git a/.verchew.ini b/.verchew.ini index 8e919925..ba9d09dc 100644 --- a/.verchew.ini +++ b/.verchew.ini @@ -1,41 +1,27 @@ [Make] cli = make - version = GNU Make - [Python] cli = python +version = Python 3. -version = Python 3.5. +[pipenv] +cli = pipenv +versions = 10. | 11. [Git] cli = git - version = 2. - -[virtualenv] - -cli = virtualenv - -version = 15. - - -[pandoc] - -cli = pandoc - -version = 1. - - [Graphviz] cli = dot cli_version_arg = -V - version = 2. +optional = true +message = This is only needed to generate UML diagrams for documentation. diff --git a/CHANGELOG.md b/CHANGELOG.md index e29a9506..2b22b82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,73 +1,81 @@ # Revision History -## 1.4 (2017/03/21) +## 1.5 (2018-09-08) + +- Added `--keep-location` option on `uninstall` (@DavidWatkins). +- Added feature to enable sparse checkouts. See the docs for further information. (@xenji) +- **BREAKING**: Removed confusing `--lock` option on `update` command in favor of just using the `lock` command. +- **BREAKING**: Renamed `--no-lock` to `--skip-lock` on `update` command. +- **BREAKING**: Renamed `--no-dirty` to `--fail-if-dirty` on `list` command. + +## 1.4 (2017-03-21) - Allow config files to exist in subdirectories of the main project. - Added `${GITMAN_CACHE}` to customize the repository cache location. -## 1.3 (2017/02/03) +## 1.3 (2017-02-03) - Added `init` command to generate sample config files. - Added support for post-install scripts on dependencies. - Updated config format to support `null` for links. -## 1.2 (2017/01/08) +## 1.2 (2017-01-08) - Added preliminary Windows support (@StudioEtrange). -## 1.1 (2017/01/06) +## 1.1 (2017-01-06) - Added coloring to the command-line output. - Fixed issue where `` could be saved as a locked revision. -## 1.0.2 (2016/07/28) +## 1.0.2 (2016-07-28) - Moved documentation to http://gitman.readthedocs.io/. -## 1.0.1 (2016/05/31) +## 1.0.1 (2016-05-31) - Replaced calls to `git remote add origin` with `git remote set-url origin`. -## 1.0 (2016/05/22) +## 1.0 (2016-05-22) - Initial stable release. -## 0.11 (2016/05/10) +## 0.11 (2016-05-10) - Removed dependency on `sh` to support Cygwin/MinGW/etc. on Windows. - Dropped Python 3.4 support for `subprocess` and `*args` improvements. - **BREAKING**: Renamed config file key `dir` to `name`. -## 0.10 (2016/04/14) +## 0.10 (2016-04-14) - Added `show` command to display dependency and internal paths. -## 0.9 (2016/03/31) +## 0.9 (2016-03-31) - Added `edit` command to launch the config file. - Depth now defaults to 5 to prevent infinite recursion. - Fixed handling of source lists containing different dependencies. -## 0.8.3 (2016/03/14) +## 0.8.3 (2016-03-14) - Renamed to GitMan. -## 0.8.2 (2016/02/24) +## 0.8.2 (2016-02-24) - Updated to YORM v0.6. -## 0.8.1 (2016/01/21) +## 0.8.1 (2016-01-21) - Added an error message when attempting to lock invalid repositories. -## 0.8 (2016/01/13) +## 0.8 (2016-01-13) - Switched to using repository mirrors to speed up cloning. - Disabled automatic fetching on install. - Added `--fetch` option on `install` to always fetch. - Now displaying `git status` output when there are changes. -## 0.7 (2015/12/22) +## 0.7 (2015-12-22) - Fixed `git remote rm` command (@hdnivara). - Now applying the `update` dependency filter to locking as well. @@ -75,12 +83,12 @@ - Added `lock` command to manually save all dependency versions. - Now requiring `--lock` option on `update` to explicitly lock dependencies. -## 0.6 (2015/11/13) +## 0.6 (2015-11-13) - Added the ability to filter the dependency list on `install` and `update`. - Added `--depth` option to limit dependency traversal on `install`, `update`, and `list`. -## 0.5 (2015/10/20) +## 0.5 (2015-10-20) - Added Git plugin support via: `git deps`. - Removed `--no-clean` option (now the default) on `install` and `update`. @@ -90,15 +98,15 @@ - Disabled warnings when running `install` without locked sources. - Added `--no-lock` option to disable version recording. -## 0.4.2 (2015/10/18) +## 0.4.2 (2015-10-18) - Fixed crash when running with some sources missing. -## 0.4.1 (2015/09/24) +## 0.4.1 (2015-09-24) - Switched to cloning for initial working tree creation. -## 0.4 (2015/09/18) +## 0.4 (2015-09-18) - Replaced `install` command with `update`. - Updated `install` command to use locked dependency versions. @@ -106,55 +114,55 @@ - Now requiring `--force` to `uninstall` with uncommitted changes. - Updated `list` command to show full shell commands. -## 0.3.1 (2015/09/09) +## 0.3.1 (2015-09-09) - Ensures files are not needlessly reloaded with newer versions of YORM. -## 0.3 (2015/06/26) +## 0.3 (2015-06-26) - Added `--no-clean` option to disable removing untracked files. - Added support for `rev-parse` dates as the dependency `rev`. -## 0.2.5 (2015/06/15) +## 0.2.5 (2015-06-15) - Added `--quiet` option to hide warnings. -## 0.2.4 (2015/05/19) +## 0.2.4 (2015-05-19) - Now hiding YORM logging bellow warnings. -## 0.2.3 (2015/05/17) +## 0.2.3 (2015-05-17) - Upgraded to YORM v0.4. -## 0.2.2 (2015/05/04) +## 0.2.2 (2015-05-04) - Specified YORM < v0.4. -## 0.2.1 (2015/03/12) +## 0.2.1 (2015-03-12) - Added automatic remote branch tracking in dependencies. - Now requiring `--force` when there are untracked files. -## 0.2 (2015/03/10) +## 0.2 (2015-03-10) - Added `list` command to display current URLs/SHAs. -## 0.1.4 (2014/02/27) +## 0.1.4 (2014-02-27) - Fixed an outdated index when checking for changes. -## 0.1.3 (2014/02/27) +## 0.1.3 (2014-02-27) - Fixed extra whitespace when logging shell output. -## 0.1.2 (2014/02/27) +## 0.1.2 (2014-02-27) - Added `--force` argument to: - overwrite uncommitted changes - create symbolic links in place of directories - Added live shell command output with `-vv` argument. -## 0.1 (2014/02/24) +## 0.1 (2014-02-24) - Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69897704..19feb0de 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: @@ -49,7 +49,7 @@ $ make watch Build the documentation: ```sh -$ make doc +$ make docs ``` ### Static Analysis @@ -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/LICENSE.md b/LICENSE.md index 35f0336e..56160e6c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,8 +1,6 @@ -# License - **The MIT License (MIT)** -Copyright © 2017, Jace Browning +Copyright © 2015, 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 diff --git a/Makefile b/Makefile index 40512ec2..21c7d9e3 100644 --- a/Makefile +++ b/Makefile @@ -8,60 +8,14 @@ PACKAGES := $(PACKAGE) tests CONFIG := $(wildcard *.py) MODULES := $(wildcard $(PACKAGE)/*.py) -# Python settings -ifndef TRAVIS - PYTHON_MAJOR ?= 3 - PYTHON_MINOR ?= 5 -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 - # 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 -endif - # Virtual environment paths -ifdef TRAVIS - ENV := $(shell dirname $(shell dirname $(shell which $(SYS_PYTHON))))/ -else - ENV := env -endif -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 +export PIPENV_VENV_IN_PROJECT=true +export PIPENV_IGNORE_VIRTUALENVS=true +VENV := .venv -# Virtual environment executables -PYTHON := $(BIN)/python -PIP := $(BIN)/pip -EASY_INSTALL := $(BIN)/easy_install -SNIFFER := $(BIN)/sniffer -HONCHO := $(ACTIVATE) && $(BIN)/honcho +# MAIN TASKS ################################################################## -# MAIN TASKS ################################################################### +SNIFFER := pipenv run sniffer .PHONY: all all: install @@ -75,61 +29,44 @@ watch: install .clean-test ## Continuously run all CI tasks when files chanage .PHONY: demo demo: install - $(BIN)/gitman install --force # some scripts have intentional errors - $(BIN)/gitman update --force # some scripts have intentional errors - $(BIN)/gitman list - $(BIN)/gitman lock - $(BIN)/gitman uninstall + pipenv run python setup.py develop + pipenv run gitman install --force # some scripts have intentional errors + pipenv run gitman update --force # some scripts have intentional errors + pipenv run gitman list + pipenv run gitman lock + pipenv run gitman uninstall -# SYSTEM DEPENDENCIES ########################################################## +# SYSTEM DEPENDENCIES ######################################################### .PHONY: doctor doctor: ## Confirm system dependencies are available bin/verchew -# PROJECT DEPENDENCIES ######################################################### +# PROJECT DEPENDENCIES ######################################################## -DEPS_CI := $(ENV)/.install-ci -DEPS_DEV := $(ENV)/.install-dev -DEPS_BASE := $(ENV)/.install-base +DEPENDENCIES = $(VENV)/.pipenv-$(shell bin/checksum Pipfile* setup.py) .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 $(PYTHON) - $(PYTHON) setup.py develop - @ touch $@ # flag to indicate dependencies are installed +install: $(DEPENDENCIES) -$(PIP): $(PYTHON) - $(PYTHON) -m pip install --upgrade pip setuptools +$(DEPENDENCIES): + pipenv run python setup.py develop + pipenv install --dev @ touch $@ -$(PYTHON): - $(SYS_PYTHON) -m venv $(ENV) +# CHECKS ###################################################################### -# CHECKS ####################################################################### - -PYLINT := $(BIN)/pylint -PYCODESTYLE := $(BIN)/pycodestyle -PYDOCSTYLE := $(BIN)/pydocstyle +ISORT := pipenv run isort +PYLINT := pipenv run pylint +PYCODESTYLE := pipenv run pycodestyle +PYDOCSTYLE := pipenv run pydocstyle .PHONY: check -check: pylint pycodestyle pydocstyle ## Run linters and static analysis +check: isort pylint pycodestyle pydocstyle ## Run linters and static analysis + +.PHONY: isort +isort: install + $(ISORT) $(PACKAGES) $(CONFIG) --recursive --apply .PHONY: pylint pylint: install @@ -143,63 +80,69 @@ pycodestyle: install pydocstyle: install $(PYDOCSTYLE) $(PACKAGES) $(CONFIG) -# TESTS ######################################################################## +# 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_OPTIONS := --random --random-seed=$(RANDOM_SEED) +ifdef DISABLE_COVERAGE +PYTEST_OPTIONS += --no-cov --disable-warnings +endif +PYTEST_RERUN_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 - @- mv $(FAILURES).bak $(FAILURES) + @ ( mv $(FAILURES) $(FAILURES).bak || true ) > /dev/null 2>&1 + $(PYTEST) $(PACKAGE) $(PYTEST_OPTIONS) + @ ( mv $(FAILURES).bak $(FAILURES) || true ) > /dev/null 2>&1 $(COVERAGE_SPACE) $(REPOSITORY) unit .PHONY: test-int test-int: install - @ if test -e $(FAILURES); then TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi + @ if test -e $(FAILURES); then TEST_INTEGRATION=true $(PYTEST) tests $(PYTEST_RERUN_OPTIONS); fi @ rm -rf $(FAILURES) - TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS) tests --junitxml=$(REPORTS)/integration.xml + TEST_INTEGRATION=true $(PYTEST) tests $(PYTEST_OPTIONS) $(COVERAGE_SPACE) $(REPOSITORY) integration .PHONY: test-all test-all: install - @ if test -e $(FAILURES); then TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(PACKAGES); fi + @ if test -e $(FAILURES); then TEST_INTEGRATION=true $(PYTEST) $(PACKAGES) $(PYTEST_RERUN_OPTIONS); fi @ rm -rf $(FAILURES) - TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml + TEST_INTEGRATION=true $(PYTEST) $(PACKAGES) $(PYTEST_OPTIONS) $(COVERAGE_SPACE) $(REPOSITORY) overall .PHONY: read-coverage read-coverage: - $(OPEN) htmlcov/index.html + bin/open htmlcov/index.html -# DOCUMENTATION ################################################################ +# 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 +.PHONY: docs +docs: uml mkdocs ## Generate documentation + +.PHONY: docs/demo.gif +docs/demo.gif: + @ sleep 3; clear; sleep 1 + gitman install --force + @ sleep 3; clear; sleep 1 + gitman list + @ sleep 3; clear; sleep 1 + gitman lock + @ sleep 3; clear; sleep 1 + gitman uninstall .PHONY: uml uml: install docs/*.png @@ -208,12 +151,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 @@ -225,27 +162,27 @@ $(MKDOCS_INDEX): mkdocs.yml docs/*.md .PHONY: mkdocs-live mkdocs-live: mkdocs - eval "sleep 3; open http://127.0.0.1:8000" & + eval "sleep 3; bin/open http://127.0.0.1:8000" & $(MKDOCS) serve -# BUILD ######################################################################## +# 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).* +.PHONY: build +build: dist + .PHONY: dist dist: install $(DIST_FILES) -$(DIST_FILES): $(MODULES) README.rst CHANGELOG.rst +$(DIST_FILES): $(MODULES) 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 $@ $< + pipenv run python setup.py check --strict --metadata + pipenv run python setup.py sdist + pipenv run python setup.py bdist_wheel .PHONY: exe exe: install $(EXE_FILES) @@ -256,71 +193,47 @@ $(EXE_FILES): $(MODULES) $(PROJECT).spec $(PROJECT).spec: $(PYINSTALLER_MAKESPEC) $(PACKAGE)/__main__.py --onefile --windowed --name=$(PROJECT) -# RELEASE ###################################################################### - -TWINE := $(BIN)/twine +# RELEASE ##################################################################### -.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 +TWINE := pipenv run twine .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 ###################################################################### +upload: dist ## Upload the current version to PyPI + git diff --name-only --exit-code + $(TWINE) upload dist/*.* + bin/open https://pypi.org/project/$(PROJECT) + +# CLEANUP ##################################################################### .PHONY: clean -clean: .clean-dist .clean-test .clean-doc .clean-build ## Delete all generated and temporary files +clean: .clean-build .clean-docs .clean-test .clean-install ## Delete all generated and temporary files .PHONY: clean-all -clean-all: clean .clean-env .clean-workspace +clean-all: clean + rm -rf $(VENV) -.PHONY: .clean-build -.clean-build: +.PHONY: .clean-install +.clean-install: 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-docs +.clean-docs: + rm -rf *.rst docs/apidocs *.html docs/*.png site -.PHONY: .clean-workspace -.clean-workspace: - rm -rf *.sublime-workspace +.PHONY: .clean-build +.clean-build: + rm -rf *.spec dist build -# HELP ######################################################################### +# 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}' + @ 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/Pipfile b/Pipfile new file mode 100644 index 00000000..93b2b727 --- /dev/null +++ b/Pipfile @@ -0,0 +1,48 @@ +[[source]] + +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[requires] + +python_version = "3" + +[packages] + +gitman = { path = ".", editable = true } + +[dev-packages] + +# Linters +pylint = "~=2.1" +pycodestyle = "~=2.3" +pydocstyle = "~=2.0" + +# Testing +pytest = "~=3.3" +pytest-describe = "*" +pytest-expecter = "*" +pytest-random = "*" +pytest-cov = "*" +freezegun = "*" + +# Reports +coverage-space = "*" + +# Documentation +mkdocs = "*" +docutils = "*" +pygments = "*" + +# Build +wheel = "*" +pyinstaller = "*" + +# Release +twine = "*" + +# Tooling +sniffer = "*" +MacFSEvents = { version = "*", sys_platform = "== 'darwin'" } +pync = { version = "*", sys_platform = "== 'darwin'" } diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..6a4a7ec3 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,612 @@ +{ + "_meta": { + "hash": { + "sha256": "f21e5911273f4c2b13efd4d467e9f1db377738bb2b17dc19936a5af3ac35dcbc" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "gitman": { + "editable": true, + "path": "." + }, + "parse": { + "hashes": [ + "sha256:c3cdf6206f22aeebfa00e5b954fcfea13d1b2dc271c75806b6025b94fb490939" + ], + "version": "==1.8.4" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, + "simplejson": { + "hashes": [ + "sha256:046ebeb1ac98d8fd1518977d7ee4003089981d80237a3e4b1b968b9be44c9dbb", + "sha256:05ab5ad4a514c7b84868d9c1bb84fbd2162a77f6d7e01a6f1a27caea50cb3738", + "sha256:15f04040c4fb5a36f07d580c8519f19b3f96fc475ffa3b355859132c8ee23095", + "sha256:1ad7e6621226e2e8e5e50cf7632221731ab5fa8f752e372a5a7bd03a9c44ae53", + "sha256:1d3c5707b3556b9d2801b7a896eec4c03ec66768a68a3594b98fcbf96ce33874", + "sha256:20c626174a3cfcc69c783930ac2d5daa72787a8e26398e33c978065a51cc8bf4", + "sha256:22682801650d2d0362b3ab332060a8ba38995e5ba1a059c0edeb9f9ab79938ea", + "sha256:2b36e41457ccc9e1c1795acd80834e3db6fc65ff9dff7baca4d6d8be1ef04eec", + "sha256:43d58832225a14b399f8686632848189677010168fb899098fd07356868f64e8", + "sha256:4f69a279a64627905303a388c8096298eda8bb058f0c9b49f0f4bd7bc28f5392", + "sha256:5505ea6da9f2e41829f6c18180457f4082aaa71e2ad543aff05c3fdbcb496c61", + "sha256:60edef86db53b9e260a8dc120b2f388d6832655b0577a40d048b6312b2928e8e", + "sha256:6e8ec1dfcda34e84b927a78ee08bdbeaa0a4aeaa031c7a36c39a7435e1049a67", + "sha256:7c2806ff87cf542b3042cee8060f1ba072cf246b697e7e91394511146379e2fd", + "sha256:83b7fc32ed02f42af3e1eb5cf853408e422bbaf3a6a6c9d4be67e1ec40c15a79", + "sha256:89561529582a7b0ed60a73d94b467bbedcded84e95f1789e508d6ef8ecc7711a", + "sha256:8ffe354a9aa48001288e224c1b34a6f7a5b63288b056879c5a7e4549874c8914", + "sha256:aab55363c9f582aea6f7b5bc0672c7a3de1e3a71b14d720a05c1c752f5d0f34c", + "sha256:abdecaa43a220e6fa395a5ba1f34bfd7bcb9f45fdf691727f1017bf79aebe805", + "sha256:b13b69ea5f82a8a930d8027cb48269857ff79bf52b5d8caffda2d701bb46702b", + "sha256:b91325d70e53134d5e6e5d03161366dc548ce40180c7adc705a32e9fb650bb32", + "sha256:cdf2f653d41ba1158a20c174b7312632ac2aaa726d73eb399e8a8f7be2f356fb", + "sha256:d6c9443ef04a8ae34fbaca2c99ace2ecb45727922d1fe0632795e345419e0dc3", + "sha256:d77ab2647a28cbc361e24708e3332721e06b82078df91c7b93a963a68a895d49", + "sha256:d92433c09b8256686f356219d5340ad5a3b7d7485b045791656159086f7bef25", + "sha256:d9b78e08e7d46144126485daed7813b57ebc48c59e11ae46b1e7e66b08628786", + "sha256:dd6addb95308a3b0df0b028dab032824e9ce86bd21c7c1c3afb9c09989e3bc82", + "sha256:fe8d1695cdfcfa738300f38aa33b1467c90ec7e7086f17a582c45084e48b3b9f" + ], + "markers": "python_version != '3.1.*' and python_version >= '2.5' and python_version != '3.2.*' and python_version != '3.0.*'", + "version": "==3.16.1" + }, + "yorm": { + "hashes": [ + "sha256:863674ad8e1cfe5b8aa42e333039c0f54d591bb0d28ff9d4299b8109a02239f9", + "sha256:aa540ccb087a9c3a8eac385073a2da1c5362591b45dec7a9a2cb212e898d9f84" + ], + "version": "==1.6" + } + }, + "develop": { + "altgraph": { + "hashes": [ + "sha256:d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", + "sha256:ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c" + ], + "version": "==0.16.1" + }, + "astroid": { + "hashes": [ + "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", + "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + ], + "version": "==2.0.4" + }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "backports.shutil-get-terminal-size": { + "hashes": [ + "sha256:0975ba55054c15e346944b38956a4c9cbee9009391e41b86c68990effb8c1f64", + "sha256:713e7a8228ae80341c70586d1cc0a8caa5207346927e23d09dcbcaf18eadec80" + ], + "version": "==1.0.0" + }, + "certifi": { + "hashes": [ + "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", + "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" + ], + "version": "==2018.8.24" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "version": "==6.7" + }, + "colorama": { + "hashes": [ + "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", + "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" + ], + "version": "==0.3.9" + }, + "coverage": { + "hashes": [ + "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", + "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", + "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", + "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", + "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", + "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", + "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", + "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", + "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", + "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", + "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", + "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", + "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", + "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", + "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", + "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", + "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", + "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", + "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", + "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", + "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", + "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", + "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", + "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", + "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", + "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", + "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", + "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", + "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", + "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", + "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6'", + "version": "==4.5.1" + }, + "coverage-space": { + "hashes": [ + "sha256:ab48b9729e54972708a6430321a0e552c10ece7c4561010d669484f453fa4e03", + "sha256:e47459028a0580d916ac3f3ccfe2cf03d1d073b3284da05c4a09f5b05114ee74" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "index": "pypi", + "version": "==0.14" + }, + "freezegun": { + "hashes": [ + "sha256:703caac155dcaad61f78de4cb0666dca778d854dfb90b3699930adee0559a622", + "sha256:94c59d69bb99c9ec3ca5a3adb41930d3ea09d2a9756c23a02d89fa75646e78dd" + ], + "index": "pypi", + "version": "==0.3.10" + }, + "future": { + "hashes": [ + "sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb" + ], + "version": "==0.16.0" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", + "version": "==4.3.4" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "livereload": { + "hashes": [ + "sha256:583179dc8d49b040a9da79bd33de59e160d2a8802b939e304eb359a4419f6498", + "sha256:dd4469a8f5a6833576e9f5433f1439c306de15dbbfeceabd32479b1123380fa5" + ], + "version": "==2.5.2" + }, + "macfsevents": { + "hashes": [ + "sha256:1324b66b356051de662ba87d84f73ada062acd42b047ed1246e60a449f833e10" + ], + "index": "pypi", + "markers": "sys_platform == 'darwin'", + "version": "==0.8.1" + }, + "macholib": { + "hashes": [ + "sha256:ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1", + "sha256:c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db" + ], + "version": "==1.11" + }, + "markdown": { + "hashes": [ + "sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f", + "sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81" + ], + "version": "==2.6.11" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mkdocs": { + "hashes": [ + "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", + "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" + ], + "index": "pypi", + "version": "==1.0.4" + }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "nose": { + "hashes": [ + "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", + "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", + "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + ], + "version": "==1.3.7" + }, + "pefile": { + "hashes": [ + "sha256:4c5b7e2de0c8cb6c504592167acf83115cbbde01fe4a507c16a1422850e86cd6" + ], + "version": "==2018.8.8" + }, + "pkginfo": { + "hashes": [ + "sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474", + "sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee" + ], + "version": "==1.4.2" + }, + "pluggy": { + "hashes": [ + "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", + "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", + "version": "==0.7.1" + }, + "py": { + "hashes": [ + "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", + "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", + "version": "==1.6.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", + "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + ], + "index": "pypi", + "version": "==2.4.0" + }, + "pydocstyle": { + "hashes": [ + "sha256:08a870edc94508264ed90510db466c6357c7192e0e866561d740624a8fc7d90c", + "sha256:4d5bcde961107873bae621f3d580c3e35a426d3687ffc6f8fb356f6628da5a97", + "sha256:af9fcccb303899b83bec82dc9a1d56c60fc369973223a5e80c3dfa9bdf984405" + ], + "index": "pypi", + "version": "==2.1.1" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "index": "pypi", + "version": "==2.2.0" + }, + "pyinstaller": { + "hashes": [ + "sha256:715f81f24b1ef0e5fe3b3c71e7540551838e46e9de30882aa7c0a521147fd1ce" + ], + "index": "pypi", + "version": "==3.3.1" + }, + "pylint": { + "hashes": [ + "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", + "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + ], + "index": "pypi", + "version": "==2.1.1" + }, + "pync": { + "hashes": [ + "sha256:38b9e61735a3161f9211a5773c5f5ea698f36af4ff7f77fa03e8d1ff0caa117f" + ], + "index": "pypi", + "markers": "sys_platform == 'darwin'", + "version": "==2.0.3" + }, + "pytest": { + "hashes": [ + "sha256:453cbbbe5ce6db38717d282b758b917de84802af4288910c12442984bde7b823", + "sha256:a8a07f84e680482eb51e244370aaf2caa6301ef265f37c2bdefb3dd3b663f99d" + ], + "index": "pypi", + "version": "==3.8.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", + "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" + ], + "index": "pypi", + "version": "==2.6.0" + }, + "pytest-describe": { + "hashes": [ + "sha256:bd6be131452b7822c872735ffe53ce3931b3b80cbbad1647c2b482cc9ef3d00e" + ], + "index": "pypi", + "version": "==0.11.1" + }, + "pytest-expecter": { + "hashes": [ + "sha256:1c8e9ab98ddd576436b61a7ba61ea11cfa5a3fc6b00288ce9e91e9dd770daf19", + "sha256:27c93dfe87e2f4d28c525031be68d3f89457e3315241d97ee15f7689544e0e37" + ], + "index": "pypi", + "version": "==1.3" + }, + "pytest-random": { + "hashes": [ + "sha256:92f25db8c5d9ffc20d90b51997b914372d6955cb9cf1f6ead45b90514fc0eddd" + ], + "index": "pypi", + "version": "==0.2" + }, + "python-dateutil": { + "hashes": [ + "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", + "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + ], + "version": "==2.7.3" + }, + "python-termstyle": { + "hashes": [ + "sha256:6faf42ba42f2826c38cf70dacb3ac51f248a418e48afc0e36593df11cf3ab1d2", + "sha256:f42a6bb16fbfc5e2c66d553e7ad46524ea833872f75ee5d827c15115fafc94e2" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", + "version": "==0.1.10" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "version": "==2.19.1" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", + "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" + ], + "version": "==0.8.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "sniffer": { + "hashes": [ + "sha256:e8a0daa4c51dff3d00482b45dc9b978159100a8d5a7df327c28ed96586559970", + "sha256:e90c1ad4bd3c31a5fad8e03d45dfc83377b31420aa0779f17280c817ce0c9dd8" + ], + "index": "pypi", + "version": "==0.4.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + ], + "version": "==1.2.1" + }, + "tornado": { + "hashes": [ + "sha256:1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448", + "sha256:4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582", + "sha256:5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046", + "sha256:6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9", + "sha256:a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f", + "sha256:c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866", + "sha256:d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", + "version": "==5.1" + }, + "tqdm": { + "hashes": [ + "sha256:5ef526702c0d265d5a960a3b27f3971fac13c26cf0fb819294bfa71fc6026c88", + "sha256:a3364bd83ce4777320b862e3c8a93d7da91e20a95f06ef79bed7dd71c654cafa" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", + "version": "==4.25.0" + }, + "twine": { + "hashes": [ + "sha256:08eb132bbaec40c6d25b358f546ec1dc96ebd2638a86eea68769d9e67fe2b129", + "sha256:2fd9a4d9ff0bcacf41fdc40c8cb0cfaef1f1859457c9653fd1b92237cc4e9f25" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.1.0" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "markers": "python_version != '3.0.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.6'", + "version": "==1.23" + }, + "wheel": { + "hashes": [ + "sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c", + "sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f" + ], + "index": "pypi", + "version": "==0.31.1" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/README.md b/README.md index 6965e081..ef76a1ff 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -Unix: [![Build Status](https://travis-ci.org/jacebrowning/gitman.svg?branch=develop)](https://travis-ci.org/jacebrowning/gitman) Windows: [![Windows Build Status](https://img.shields.io/appveyor/ci/jacebrowning/gitman/develop.svg)](https://ci.appveyor.com/project/jacebrowning/gitman)
Metrics: [![Coverage Status](https://img.shields.io/coveralls/jacebrowning/gitman/develop.svg)](https://coveralls.io/r/jacebrowning/gitman) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jacebrowning/gitman.svg)](https://scrutinizer-ci.com/g/jacebrowning/gitman/?branch=develop)
Usage: [![PyPI Version](https://img.shields.io/pypi/v/GitMan.svg)](https://pypi.python.org/pypi/GitMan) [![PyPI Downloads](https://img.shields.io/pypi/dm/gitman.svg)](https://pypi.python.org/pypi/GitMan) +Unix: [![Build Status](https://travis-ci.org/jacebrowning/gitman.svg?branch=develop)](https://travis-ci.org/jacebrowning/gitman) Windows: [![Windows Build Status](https://img.shields.io/appveyor/ci/jacebrowning/gitman/develop.svg)](https://ci.appveyor.com/project/jacebrowning/gitman)
Metrics: [![Coverage Status](https://img.shields.io/coveralls/jacebrowning/gitman/develop.svg)](https://coveralls.io/r/jacebrowning/gitman) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jacebrowning/gitman.svg)](https://scrutinizer-ci.com/g/jacebrowning/gitman/?branch=develop)
Usage: [![PyPI Version](https://img.shields.io/pypi/v/GitMan.svg)](https://pypi.org/project/GitMan) # Overview -GitMan is a language-agnostic "dependency manager" using Git. It aims to serve as a submodules replacement and provides advanced options for managing versions of nested Git repositories. +GitMan is a language-agnostic dependency manager using Git. It aims to serve as a submodules replacement and provides advanced options for managing versions of nested Git repositories. ![demo](https://raw.githubusercontent.com/jacebrowning/gitman/develop/docs/demo.gif) @@ -29,7 +29,7 @@ $ cd gitman $ python setup.py install ``` -## Setup +## Configuration Generate a sample config file: @@ -54,6 +54,11 @@ sources: rev: master scripts: - chmod a+x truffleHog.py +- name: fontawesome + repo: https://github.com/FortAwesome/Font-Awesome + sparse_paths: + - fonts/* + rev: master ``` Ignore the dependency storage location: diff --git a/bin/checksum b/bin/checksum new file mode 100755 index 00000000..21cb84da --- /dev/null +++ b/bin/checksum @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import hashlib +import sys + + +def run(paths): + hash_md5 = hashlib.md5() + + for path in paths: + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b''): + hash_md5.update(chunk) + + print(hash_md5.hexdigest()) + + +if __name__ == '__main__': + run(sys.argv[1:]) diff --git a/bin/open b/bin/open new file mode 100755 index 00000000..f7ae38a4 --- /dev/null +++ b/bin/open @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + + +COMMANDS = { + 'linux': "open", + 'win32': "cmd /c start", + 'cygwin': "cygstart", + 'darwin': "open", +} + + +def run(path): + command = COMMANDS.get(sys.platform, "open") + os.system(command + ' ' + path) + + +if __name__ == '__main__': + run(sys.argv[-1]) diff --git a/bin/verchew b/bin/verchew index 359e69f1..ee946972 100755 --- a/bin/verchew +++ b/bin/verchew @@ -21,32 +21,71 @@ # 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. +# +# Source: https://github.com/jacebrowning/verchew +# Documentation: https://verchew.readthedocs.io +# Package: https://pypi.org/project/verchew from __future__ import unicode_literals +import argparse +import logging import os import sys -import argparse +from collections import OrderedDict +from subprocess import PIPE, STDOUT, Popen + + try: import configparser # Python 3 except ImportError: import ConfigParser as configparser # Python 2 -from collections import OrderedDict -from subprocess import Popen, PIPE, STDOUT -import logging -__version__ = '1.0' +__version__ = '1.3' PY2 = sys.version_info[0] == 2 -CONFIG_FILENAMES = ['.verchew', '.verchewrc', 'verchew.ini', '.verchew.ini'] +CONFIG_FILENAMES = [ + 'verchew.ini', + '.verchew.ini', + '.verchewrc', + '.verchew', +] +SAMPLE_CONFIG = """ +[Python] + +cli = python +versions = Python 3.5 | Python 3.6 + +[Legacy Python] + +cli = python2 +version = Python 2.7 + +[virtualenv] + +cli = virtualenv +version = 15. +message = Only required with Python 2. + +[Make] + +cli = make +version = GNU Make +optional = true + +""".strip() STYLE = { + "~": "✔", + "*": "⭑", + "?": "⚠", "x": "✘", - "~": "✔" } COLOR = { "x": "\033[91m", # red "~": "\033[92m", # green + "?": "\033[93m", # yellow + "*": "\033[94m", # cyan None: "\033[0m", # reset } @@ -60,7 +99,7 @@ def main(): log.debug("PWD: %s", os.getenv('PWD')) log.debug("PATH: %s", os.getenv('PATH')) - path = find_config(args.root) + path = find_config(args.root, generate=args.init) config = parse_config(path) if not check_dependencies(config) and args.exit_code: @@ -72,12 +111,14 @@ def parse_args(): version = "%(prog)s v" + __version__ parser.add_argument('--version', action='version', version=version) - parser.add_argument('-v', '--verbose', action='count', default=0, - help="enable verbose logging") parser.add_argument('-r', '--root', metavar='PATH', help="specify a custom project root directory") + parser.add_argument('--init', action='store_true', + help="generate a sample configuration file") parser.add_argument('--exit-code', action='store_true', help="return a non-zero exit code on failure") + parser.add_argument('-v', '--verbose', action='count', default=0, + help="enable verbose logging") args = parser.parse_args() @@ -95,23 +136,40 @@ def configure_logging(count=0): logging.basicConfig(level=level, format="%(levelname)s: %(message)s") -def find_config(root=None, config_filenames=None): +def find_config(root=None, filenames=None, generate=False): root = root or os.getcwd() - config_filenames = config_filenames or CONFIG_FILENAMES + filenames = filenames or CONFIG_FILENAMES path = None log.info("Looking for config file in: %s", root) - log.debug("Filename options: %s", ", ".join(config_filenames)) + log.debug("Filename options: %s", ", ".join(filenames)) for filename in os.listdir(root): - if filename in config_filenames: + if filename in filenames: path = os.path.join(root, filename) log.info("Found config file: %s", path) return path + if generate: + path = generate_config(root, filenames) + return path + msg = "No config file found in: {0}".format(root) raise RuntimeError(msg) +def generate_config(root=None, filenames=None): + root = root or os.getcwd() + filenames = filenames or CONFIG_FILENAMES + + path = os.path.join(root, filenames[0]) + + log.info("Generating sample config: %s", path) + with open(path, 'w') as config: + config.write(SAMPLE_CONFIG + '\n') + + return path + + def parse_config(path): data = OrderedDict() @@ -124,6 +182,11 @@ def parse_config(path): for name, value in config.items(section): data[section][name] = value + for name in data: + versions = data[name].get('versions', data[name].pop('version', "")) + data[name]['versions'] = versions + data[name]['patterns'] = [v.strip() for v in versions.split('|')] + return data @@ -133,12 +196,21 @@ def check_dependencies(config): for name, settings in config.items(): show("Checking for {0}...".format(name), head=True) output = get_version(settings['cli'], settings.get('cli_version_arg')) - if match_version(settings['version'], output): - show(_("~") + " MATCHED: {0}".format(settings['version'])) - success.append(_("~")) + + for pattern in settings['patterns']: + if match_version(pattern, output): + show(_("~") + " MATCHED: {0}".format(pattern)) + success.append(_("~")) + break else: - show(_("x") + " EXPECTED: {0}".format(settings['version'])) - success.append(_("x")) + if settings.get('optional'): + show(_("?") + " EXPECTED: {0}".format(settings['versions'])) + success.append(_("?")) + else: + show(_("x") + " EXPECTED: {0}".format(settings['versions'])) + success.append(_("x")) + if settings.get('message'): + show(_("*") + " MESSAGE: {0}".format(settings['message'])) show("Results: " + " ".join(success), head=True) diff --git a/docs/demo.gif b/docs/demo.gif index ce1c53b0..7bf27d5f 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/docs/interfaces/api.md b/docs/interfaces/api.md index de1a8b50..be3fa519 100644 --- a/docs/interfaces/api.md +++ b/docs/interfaces/api.md @@ -45,7 +45,7 @@ with optional arguments: - `force`: indicates uncommitted changes can be overwritten and script errors can be ignored - `clean`: indicates untracked files should be deleted from dependencies -- `lock`: indicates actual dependency versions should be recorded +- `lock`: indicates updated dependency versions should be recorded ## List @@ -79,10 +79,11 @@ with optional arguments: To delete all dependencies, call: ```python -gitman.uninstall(root=None, force=False) +gitman.uninstall(root=None, force=False, keep_location=False) ``` with optional arguments: - `root`: specifies the path to the root working tree - `force`: indicates uncommitted changes can be overwritten +- `keep_location`: indicates that the top level folder should be kept diff --git a/docs/interfaces/cli.md b/docs/interfaces/cli.md index ff7c576e..f8dbb114 100644 --- a/docs/interfaces/cli.md +++ b/docs/interfaces/cli.md @@ -15,37 +15,37 @@ $ gitman init To clone/checkout the specified dependencies, run: ```sh -gitman install +$ gitman install ``` or filter the dependency list by directory name: ```sh -gitman install +$ gitman install ``` or limit the traversal of nested dependencies: ```sh -gitman install --depth= +$ gitman install --depth= ``` It will leave untracked files alone. To delete them, run: ```sh -gitman install --clean +$ gitman install --clean ``` It will only fetch from the repository if needed. To always fetch, run: ```sh -gitman install --fetch +$ gitman install --fetch ``` It will exit with an error if there are any uncommitted changes in dependencies or a post-install script fails. To overwrite all changes or ignore script failures, run: ```sh -gitman install --force +$ gitman install --force ``` ## Update @@ -53,31 +53,31 @@ gitman install --force If any of the dependencies track a branch (rather than a specific commit), the current upstream version of that branch can be checked out by running: ```sh -gitman update +$ gitman update ``` or filter the dependency list by directory name: ```sh -gitman update +$ gitman update ``` or limit the traversal of nested dependencies: ```sh -gitman update --depth= +$ gitman update --depth= ``` This will also record the exact versions of any previously locked dependencies. Disable this behavior by instead running: ```sh -gitman update --no-lock +$ gitman update --skip-lock ``` or to additionally get the latest versions of all nested dependencies, run: ```sh -gitman update --all +$ gitman update --all ``` ## List @@ -85,13 +85,13 @@ gitman update --all To display the currently checked out dependencies, run: ```sh -gitman list +$ gitman list ``` or exit with an error if there are any uncommitted changes: ```sh -gitman list --no-dirty +$ gitman list --fail-if-dirty ``` The `list` command will also record versions in the log file. @@ -101,25 +101,19 @@ The `list` command will also record versions in the log file. To manually record the exact version of each dependency, run: ```sh -gitman lock +$ gitman lock ``` or lock down specific dependencies: ```sh -gitman lock -``` - -This can be combined with updating dependencies by running: - -```sh -gitman update --lock +$ gitman lock ``` To restore the exact versions previously checked out, run: ```sh -gitman install +$ gitman install ``` ## Uninstall @@ -127,13 +121,19 @@ gitman install To delete all dependencies, run: ```sh -gitman uninstall +$ gitman uninstall ``` If any dependencies contain uncommitted changes, instead run: ```sh -gitman uninstall --force +$ gitman uninstall --force +``` + +If you need to keep the top level folder and anything other than the dependencies: + +```sh +$ gitman uninstall --keep-location ``` ## Show @@ -141,25 +141,25 @@ gitman uninstall --force To display the path to the dependency storage location: ```sh -gitman show +$ gitman show ``` To display the path to a dependency: ```sh -gitman show +$ gitman show ``` To display the path to the config file: ```sh -gitman show --config +$ gitman show --config ``` To display the path to the log file: ```sh -gitman show --log +$ gitman show --log ``` ## Edit @@ -167,5 +167,5 @@ gitman show --log To open the existing config file: ```sh -gitman edit +$ gitman edit ``` diff --git a/docs/interfaces/plugin.md b/docs/interfaces/plugin.md index 317d1445..16bfa76f 100644 --- a/docs/interfaces/plugin.md +++ b/docs/interfaces/plugin.md @@ -7,19 +7,19 @@ To clone/checkout the specified dependencies, run: ```sh -git deps +$ git deps ``` Delete all untracked files in dependencies by instead running: ```sh -git deps --clean +$ git deps --clean ``` Git will exit with an error if there are any uncommitted changes in dependencies or a post-install script fails. To overwrite all changes or ignore script failures, run: ```sh -git deps --force +$ git deps --force ``` ## Update @@ -27,25 +27,25 @@ git deps --force If any of the dependencies track a branch (rather than a specific commit), the current upstream version of that branch can be checked out by running: ```sh -git deps --update +$ git deps --update ``` This will also record the exact versions that were checked out. Disable this behavior by instead running: ```sh -git deps --update --no-lock +$ git deps --update --skip-lock ``` Or, to additionally get the latest versions of all nested dependencies, run: ```sh -git deps --update --all +$ git deps --update --all ``` To restore the exact versions previously checked out, run: ```sh -git deps +$ git deps ``` ## List @@ -53,7 +53,7 @@ git deps To display the currently checked out dependencies, run: ```sh -git deps --list +$ git deps --list ``` ## Uninstall @@ -61,11 +61,18 @@ git deps --list To delete all dependencies, run: ```sh -git deps --uninstall +$ git deps --uninstall ``` If any dependencies contain uncommitted changes, instead run: ```sh -git deps --uninstall --force +$ git deps --uninstall --force ``` + +If you need to keep the top level folder and anything other than the dependencies: + +```sh +$ git deps --uninstall --keep-location +``` + diff --git a/docs/setup/git.md b/docs/setup/git.md index 067d840d..1cfac55f 100644 --- a/docs/setup/git.md +++ b/docs/setup/git.md @@ -18,7 +18,7 @@ then credential storage is not set up correctly. ## Stored Credentials -To use the Keychain manager on OS X, run: +To use the Keychain on macOS, run: ``` $ git config --global credential.helper osxkeychain diff --git a/docs/use-cases/sparse-checkouts.md b/docs/use-cases/sparse-checkouts.md new file mode 100644 index 00000000..8e97a3ac --- /dev/null +++ b/docs/use-cases/sparse-checkouts.md @@ -0,0 +1,20 @@ +# Using sparse checkouts + +For some use-cases, especially when dealing with monorepos, it can be useful to limit the paths that are checked out +from the reference repository. It is important to note, that this influences only the shape of the project local +checkout. The reference repository, maintained by gitman as a cache, will still be a full clone of the original repo. + +Using `sparse_paths` will use git's sparse checkout feature to just materialize the selected paths in the working tree. +As this is a [git feature](https://git-scm.com/docs/git-read-tree#_sparse_checkout), all syntax options are +available and passed unmodified to `$GIT_DIR/info/sparse-checkout`. + +The following example configuration will clone the full font-awesome repo into the cache, but the project local +clone will only contain the `fonts` directory and it's children. + +```yaml +- name: fontawesome + repo: https://github.com/FortAwesome/Font-Awesome + sparse_paths: + - fonts/* + rev: master +``` diff --git a/gitman.yml b/gitman.yml index 5120ab80..5bb23897 100644 --- a/gitman.yml +++ b/gitman.yml @@ -2,6 +2,8 @@ location: demo sources: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-branch link: scripts: @@ -9,20 +11,34 @@ sources: - make foobar - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-tag link: scripts: - - name: gitman_3 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: master@{2015-06-18 11:11:11} link: scripts: - echo "Hello, World!" - pwd +- name: gitman_4 + repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - + rev: example-branch-2 + link: + scripts: + - sources_locked: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 1de84ca1d315f81b035cd7b0ecf87ca2025cdacd link: scripts: @@ -30,14 +46,26 @@ sources_locked: - make foobar - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 7bd138fe7359561a8c2ff9d195dff238794ccc04 link: scripts: - - name: gitman_3 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 2da24fca34af3748e3cab61db81a2ae8b35aec94 link: scripts: - echo "Hello, World!" - pwd +- name: gitman_4 + repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - + rev: f50c1ac8bf27377625b0cc93ea27f8069c7b513a + link: + scripts: + - diff --git a/gitman/__init__.py b/gitman/__init__.py index e64eef3a..f5feae29 100644 --- a/gitman/__init__.py +++ b/gitman/__init__.py @@ -2,8 +2,9 @@ import sys + __project__ = 'GitMan' -__version__ = '1.4' +__version__ = '1.5.post1' CLI = 'gitman' PLUGIN = 'deps' diff --git a/gitman/__main__.py b/gitman/__main__.py index 8663f2e4..cb2b5e43 100644 --- a/gitman/__main__.py +++ b/gitman/__main__.py @@ -1,7 +1,7 @@ """Package entry point.""" # Declare itself as package if needed for better debugging support -# pylint: disable=multiple-imports,wrong-import-position,redefined-builtin +# pylint: disable=multiple-imports,wrong-import-position,redefined-builtin,used-before-assignment if __name__ == '__main__' and __package__ is None: # pragma: no cover import os, sys, importlib parent_dir = os.path.abspath(os.path.dirname(__file__)) diff --git a/gitman/cli.py b/gitman/cli.py index a982e387..05773034 100644 --- a/gitman/cli.py +++ b/gitman/cli.py @@ -2,12 +2,12 @@ """Command-line interface.""" -import sys import argparse import logging +import sys + +from . import CLI, DESCRIPTION, VERSION, commands, common, exceptions -from . import CLI, VERSION, DESCRIPTION -from . import common, exceptions, commands log = logging.getLogger(__name__) @@ -65,20 +65,16 @@ def main(args=None, function=None): # pylint: disable=too-many-statements sub.add_argument('name', nargs='*', help="list of dependencies names to update") sub.add_argument('-a', '--all', action='store_true', dest='recurse', - help="update all nested dependencies, recursively") - group = sub.add_mutually_exclusive_group() - group.add_argument('-l', '--lock', - action='store_true', dest='lock', default=None, - help="enable recording of versions for later reinstall") - group.add_argument('-L', '--no-lock', - action='store_false', dest='lock', default=None, - help="disable recording of versions for later reinstall") + help="also update all nested dependencies") + sub.add_argument('-L', '--skip-lock', + action='store_false', dest='lock', default=None, + help="disable recording of updated versions") # List parser info = "display the current version of each dependency" sub = subs.add_parser('list', description=info.capitalize() + '.', help=info, parents=[debug, project, depth], **shared) - sub.add_argument('-D', '--no-dirty', action='store_false', + sub.add_argument('-D', '--fail-if-dirty', action='store_false', dest='allow_dirty', help="fail if a source has uncommitted changes") @@ -95,6 +91,9 @@ def main(args=None, function=None): # pylint: disable=too-many-statements help=info, parents=[debug, project], **shared) sub.add_argument('-f', '--force', action='store_true', help="delete uncommitted changes in dependencies") + sub.add_argument('-k', '--keep-location', dest='keep_location', + default=False, action='store_true', + help="keep top level folder location") # Show parser info = "display the path of a dependency or internal file" @@ -161,7 +160,8 @@ def _get_command(function, namespace): # pylint: disable=too-many-statements elif namespace.command == 'uninstall': function = commands.delete kwargs.update(root=namespace.root, - force=namespace.force) + force=namespace.force, + keep_location=namespace.keep_location) elif namespace.command == 'show': function = commands.show diff --git a/gitman/commands.py b/gitman/commands.py index 150b249d..a463e147 100644 --- a/gitman/commands.py +++ b/gitman/commands.py @@ -1,12 +1,13 @@ """Functions to manage the installation of dependencies.""" -import os -import functools import datetime +import functools import logging +import os from . import common, system -from .models import load_config, Config, Source +from .models import Config, Source, load_config + log = logging.getLogger(__name__) @@ -102,7 +103,7 @@ def update(*names, root=None, depth=None, - `force`: indicates uncommitted changes can be overwritten and script errors can be ignored - `clean`: indicates untracked files should be deleted from dependencies - - `lock`: indicates actual dependency versions should be recorded + - `lock`: indicates updated dependency versions should be recorded """ log.info("%s dependencies%s: %s", @@ -209,13 +210,14 @@ def lock(*names, root=None): @restore_cwd -def delete(*, root=None, force=False): +def delete(*, root=None, force=False, keep_location=False): """Delete dependencies for a project. Optional arguments: - `root`: specifies the path to the root working tree - `force`: indicates uncommitted changes can be overwritten + - `keep_location`: delete top level folder or keep the location """ log.info("Deleting dependencies...") @@ -232,7 +234,10 @@ def delete(*, root=None, force=False): common.dedent(level=0) common.show("Deleting all dependencies...", color='message', log=False) common.newline() - config.uninstall_dependencies() + if keep_location: + config.clean_dependencies() + else: + config.uninstall_dependencies() return _display_result("delete", "Deleted", count, allow_zero=True) @@ -302,8 +307,8 @@ def _display_result(present, past, count, allow_zero=False): if count: return True - elif count is None: + if count is None: return False - else: - assert count == 0 - return allow_zero + + assert count == 0 + return allow_zero diff --git a/gitman/common.py b/gitman/common.py index eb3896db..b84eb43e 100644 --- a/gitman/common.py +++ b/gitman/common.py @@ -1,9 +1,9 @@ """Common exceptions, classes, and functions.""" -import os -import sys import argparse import logging +import os +import sys from . import settings @@ -27,7 +27,6 @@ def __init__(self, default_format, verbose_format, *args, **kwargs): self.verbose_format = verbose_format def format(self, record): - """A hack to change the formatting style dynamically.""" # pylint: disable=protected-access if record.levelno > logging.INFO: self._style._fmt = self.verbose_format @@ -37,7 +36,6 @@ def format(self, record): def positive_int(value): - """Custom `int` that must be positive.""" value = int(value) if value < 1: raise TypeError diff --git a/gitman/git.py b/gitman/git.py index 2b990f72..423e2300 100644 --- a/gitman/git.py +++ b/gitman/git.py @@ -1,12 +1,13 @@ """Utilities to call Git commands.""" -import os import logging +import os +import re from contextlib import suppress from . import common, settings -from .shell import call from .exceptions import ShellError +from .shell import call log = logging.getLogger(__name__) @@ -16,7 +17,7 @@ def git(*args, **kwargs): return call('git', *args, **kwargs) -def clone(repo, path, *, cache=settings.CACHE): +def clone(repo, path, *, cache=settings.CACHE, sparse_paths=None, rev=None): """Clone a new Git repository.""" log.debug("Creating a new repository...") @@ -28,7 +29,35 @@ def clone(repo, path, *, cache=settings.CACHE): if not os.path.isdir(reference): git('clone', '--mirror', repo, reference) - git('clone', '--reference', reference, repo, os.path.normpath(path)) + normpath = os.path.normpath(path) + if sparse_paths: + os.mkdir(normpath) + git('-C', normpath, 'init') + git('-C', normpath, 'config', 'core.sparseCheckout', 'true') + git('-C', normpath, 'remote', 'add', '-f', 'origin', reference) + + with open("%s/%s/.git/info/sparse-checkout" % + (os.getcwd(), normpath), 'w') as fd: + fd.writelines(sparse_paths) + with open("%s/%s/.git/objects/info/alternates" % + (os.getcwd(), normpath), 'w') as fd: + fd.write("%s/objects" % reference) + + # We use directly the revision requested here in order to respect, + # that not all repos have `master` as their default branch + git('-C', normpath, 'pull', 'origin', rev) + else: + git('clone', '--reference', reference, repo, os.path.normpath(path)) + + +def is_sha(rev): + """Heuristically determine whether a revision corresponds to a commit SHA. + + Any sequence of 7 to 40 hexadecimal digits will be recognized as a + commit SHA. The minimum of 7 digits is not an arbitrary choice, it + is the default length for short SHAs in Git. + """ + return re.match('^[0-9a-f]{7,40}$', rev) is not None def fetch(repo, rev=None): @@ -36,7 +65,7 @@ def fetch(repo, rev=None): git('remote', 'set-url', 'origin', repo) args = ['fetch', '--tags', '--force', '--prune', 'origin'] if rev: - if len(rev) == 40: + if is_sha(rev): pass # fetch only works with a SHA if already present locally elif '@' in rev: pass # fetch doesn't work with rev-parse diff --git a/gitman/models/__init__.py b/gitman/models/__init__.py index a71cf5e3..dede3b85 100644 --- a/gitman/models/__init__.py +++ b/gitman/models/__init__.py @@ -1,2 +1,2 @@ -from .source import Source +from .source import Source # isort:skip from .config import Config, load_config diff --git a/gitman/models/config.py b/gitman/models/config.py index 6a241eab..37d72917 100644 --- a/gitman/models/config.py +++ b/gitman/models/config.py @@ -1,12 +1,12 @@ -import os import logging +import os import yorm -from yorm.types import String, SortedList +from yorm.types import SortedList, String -from .. import common -from .. import shell from . import Source +from .. import common, shell + log = logging.getLogger(__name__) @@ -50,12 +50,11 @@ def get_path(self, name=None): base = self.location_path if name == '__config__': return self.path - elif name == '__log__': + if name == '__log__': return self.log_path - elif name: + if name: return os.path.normpath(os.path.join(base, name)) - else: - return base + return base def install_dependencies(self, *names, depth=None, update=True, recurse=False, @@ -179,8 +178,38 @@ def uninstall_dependencies(self): shell.rm(self.location_path) common.newline() + def clean_dependencies(self): + """Delete the dependency storage location.""" + for path in self.get_top_level_dependencies(): + + if path == self.location_path: + log.info("Skipped dependency: %s", path) + else: + shell.rm(path) + + common.newline() + + shell.rm(self.log_path) + + def get_top_level_dependencies(self): + """Yield the path, repository, and hash of top-level dependencies.""" + if not os.path.exists(self.location_path): + return + + shell.cd(self.location_path) + common.newline() + common.indent() + + for source in self.sources: + + yield os.path.join(self.location_path, source.name) + + shell.cd(self.location_path, _show=False) + + common.dedent() + def get_dependencies(self, depth=None, allow_dirty=True): - """Yield the path, repository URL, and hash of each dependency.""" + """Yield the path, repository, and hash of each dependency.""" if not os.path.exists(self.location_path): return @@ -219,9 +248,8 @@ def _get_sources(self, *, use_locked=None): if use_locked is True: if self.sources_locked: return self.sources_locked - else: - log.info("No locked sources, defaulting to none...") - return [] + log.info("No locked sources, defaulting to none...") + return [] sources = [] if use_locked is False: @@ -237,7 +265,8 @@ def _get_sources(self, *, use_locked=None): extras = [] for source in self.sources + self.sources_locked: if source not in sources: - log.info("Source %r missing from selected section", source.name) + log.info("Source %r missing from selected section", + source.name) extras.append(source) return sources + extras diff --git a/gitman/models/source.py b/gitman/models/source.py index 63ffd960..12f73818 100644 --- a/gitman/models/source.py +++ b/gitman/models/source.py @@ -1,11 +1,11 @@ -import os import logging +import os import warnings import yorm -from yorm.types import String, NullableString, List, AttributeDictionary +from yorm.types import AttributeDictionary, List, NullableString, String -from .. import common, exceptions, shell, git +from .. import common, exceptions, git, shell log = logging.getLogger(__name__) @@ -13,6 +13,7 @@ @yorm.attr(name=String) @yorm.attr(repo=String) +@yorm.attr(sparse_paths=List.of_type(String)) @yorm.attr(rev=String) @yorm.attr(link=NullableString) @yorm.attr(scripts=List.of_type(String)) @@ -22,13 +23,15 @@ class Source(AttributeDictionary): DIRTY = '' UNKNOWN = '' - def __init__(self, repo, name=None, rev='master', link=None, scripts=None): + def __init__(self, repo, name=None, rev='master', + link=None, scripts=None, sparse_paths=None): super().__init__() self.repo = repo self.name = self._infer_name(repo) if name is None else name self.rev = rev self.link = link self.scripts = scripts or [] + self.sparse_paths = sparse_paths or [] for key in ['name', 'repo', 'rev']: if not self[key]: @@ -42,7 +45,8 @@ def __str__(self): pattern = "'{r}' @ '{v}' in '{d}'" if self.link: pattern += " <- '{s}'" - return pattern.format(r=self.repo, v=self.rev, d=self.name, s=self.link) + return pattern.format(r=self.repo, v=self.rev, + d=self.name, s=self.link) def __eq__(self, other): return self.name == other.name @@ -59,7 +63,8 @@ def update_files(self, force=False, fetch=False, clean=True): # Clone the repository if needed if not os.path.exists(self.name): - git.clone(self.repo, self.name) + git.clone(self.repo, self.name, + sparse_paths=self.sparse_paths, rev=self.rev) # Enter the working tree shell.cd(self.name) @@ -155,19 +160,16 @@ def identify(self, allow_dirty=True, allow_missing=True): common.show(self.DIRTY, color='git_dirty', log=False) common.newline() return path, url, self.DIRTY - else: - rev = git.get_hash(_show=True) - common.show(rev, color='git_rev', log=False) - common.newline() - return path, url, rev - elif allow_missing: + rev = git.get_hash(_show=True) + common.show(rev, color='git_rev', log=False) + common.newline() + return path, url, rev + if allow_missing: return os.getcwd(), '', self.UNKNOWN - else: - - raise self._invalid_repository + raise self._invalid_repository def lock(self, rev=None): """Return a locked version of the current source.""" diff --git a/gitman/plugin.py b/gitman/plugin.py index fd20cc13..00ecadc2 100644 --- a/gitman/plugin.py +++ b/gitman/plugin.py @@ -5,10 +5,10 @@ import argparse import logging -from . import PLUGIN, NAME, __version__ -from . import common +from . import NAME, PLUGIN, __version__, common from .cli import _get_command, _run_command + PROG = 'git ' + PLUGIN DESCRIPTION = "Use {} (v{}) to install repostories.".format(NAME, __version__) @@ -40,9 +40,9 @@ def main(args=None): ) parser.add_argument('-a', '--all', action='store_true', dest='recurse', help="include nested dependencies when updating") - parser.add_argument('-L', '--no-lock', + parser.add_argument('-L', '--skip-lock', action='store_false', dest='lock', default=True, - help="skip recording of versions for later reinstall") + help="disable recording of updated versions") # Display option group.add_argument( @@ -55,6 +55,9 @@ def main(args=None): '-x', '--uninstall', const='uninstall', help="delete all installed dependencies", **shared ) + parser.add_argument('-k', '--keep-location', action='store_true', + dest='keep_location', default=False, + help='keep top level folder location') # Parse arguments namespace = parser.parse_args(args=args) diff --git a/gitman/settings.py b/gitman/settings.py index f926dbbc..1678e8e1 100644 --- a/gitman/settings.py +++ b/gitman/settings.py @@ -1,7 +1,8 @@ """Program defaults.""" -import os import logging +import os + # Cache settings CACHE = os.path.expanduser(os.getenv('GITMAN_CACHE', "~/.gitcache")) diff --git a/gitman/shell.py b/gitman/shell.py index 1c6727d7..6a1cce18 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -1,12 +1,13 @@ """Utilities to call shell programs.""" +import logging import os import subprocess -import logging from . import common from .exceptions import ShellError + CMD_PREFIX = "$ " OUT_PREFIX = "> " @@ -39,19 +40,18 @@ def call(name, *args, _show=True, _shell=False, _ignore=False): if command.returncode == 0: return output - elif _ignore: + if _ignore: log.debug("Ignored error from call to '%s'", name) return output - else: - message = ( - "An external program call failed." + "\n\n" - "In working directory: " + os.getcwd() + "\n\n" - "The following command produced a non-zero return code:" + "\n\n" + - CMD_PREFIX + program + "\n" + - command.stdout.strip() - ) - raise ShellError(message, program=program, output=output) + message = ( + "An external program call failed." + "\n\n" + "In working directory: " + os.getcwd() + "\n\n" + "The following command produced a non-zero return code:" + "\n\n" + + CMD_PREFIX + program + "\n" + + command.stdout.strip() + ) + raise ShellError(message, program=program, output=output) def mkdir(path): diff --git a/gitman/system.py b/gitman/system.py index 1821dfc2..a22599bb 100644 --- a/gitman/system.py +++ b/gitman/system.py @@ -1,9 +1,10 @@ """Interface to the operating system.""" +import logging import os import platform import subprocess -import logging + log = logging.getLogger(__name__) diff --git a/gitman/tests/conftest.py b/gitman/tests/conftest.py index 9a3596f9..9d32d556 100644 --- a/gitman/tests/conftest.py +++ b/gitman/tests/conftest.py @@ -1,7 +1,7 @@ """Unit test configuration file.""" -import os import logging +import os import pytest import yorm diff --git a/gitman/tests/test_cli.py b/gitman/tests/test_cli.py index 98378f10..547a4ad5 100644 --- a/gitman/tests/test_cli.py +++ b/gitman/tests/test_cli.py @@ -1,14 +1,14 @@ # pylint: disable=no-self-use,unused-variable,expression-not-assigned -from unittest.mock import Mock, patch import logging +from unittest.mock import Mock, patch import pytest from expecter import expect from gitman import cli from gitman.common import _Config -from gitman.exceptions import UncommittedChanges, ScriptFailure +from gitman.exceptions import ScriptFailure, UncommittedChanges class TestMain: @@ -155,26 +155,12 @@ def test_update_recursive(self, mock_update): @patch('gitman.commands.update') def test_update_no_lock(self, mock_update): """Verify the 'update' command can disable locking.""" - cli.main(['update', '--no-lock']) + cli.main(['update', '--skip-lock']) mock_update.assert_called_once_with( root=None, depth=5, force=False, clean=False, recurse=False, lock=False) - @patch('gitman.commands.update') - def test_update_lock(self, mock_update): - """Verify the 'update' command can enable locking.""" - cli.main(['update', '--lock']) - - mock_update.assert_called_once_with( - root=None, depth=5, - force=False, clean=False, recurse=False, lock=True) - - def test_update_lock_conflict(self): - """Verify the 'update' command cannot specify both locking options.""" - with pytest.raises(SystemExit): - cli.main(['update', '--lock', '--no-lock']) - @patch('gitman.commands.update') def test_update_specific_sources(self, mock_install): """Verify individual dependencies can be installed.""" @@ -216,7 +202,7 @@ def test_list_root(self, mock_display): @patch('gitman.commands.display') def test_list_no_dirty(self, mock_display): """Verify the 'list' command can be set to fail when dirty.""" - cli.main(['list', '--no-dirty']) + cli.main(['list', '--fail-if-dirty']) mock_display.assert_called_once_with( root=None, depth=5, allow_dirty=False) @@ -252,7 +238,7 @@ def test_uninstall(self, mock_uninstall): cli.main(['uninstall']) mock_uninstall.assert_called_once_with( - root=None, force=False) + root=None, force=False, keep_location=False) @patch('gitman.commands.delete') def test_uninstall_root(self, mock_uninstall): @@ -260,7 +246,7 @@ def test_uninstall_root(self, mock_uninstall): cli.main(['uninstall', '--root', 'mock/path/to/root']) mock_uninstall.assert_called_once_with( - root='mock/path/to/root', force=False) + root='mock/path/to/root', force=False, keep_location=False) @patch('gitman.commands.delete') def test_uninstall_force(self, mock_uninstall): @@ -268,7 +254,15 @@ def test_uninstall_force(self, mock_uninstall): cli.main(['uninstall', '--force']) mock_uninstall.assert_called_once_with( - root=None, force=True) + root=None, force=True, keep_location=False) + + @patch('gitman.commands.delete') + def test_uninstall_keep_location(self, mock_uninstall): + """Verify the 'uninstall' command can be run with keep_location.""" + cli.main(['uninstall', '--keep-location']) + + mock_uninstall.assert_called_once_with( + root=None, force=False, keep_location=True) def describe_show(): diff --git a/gitman/tests/test_git.py b/gitman/tests/test_git.py index 978401b7..60eeb436 100644 --- a/gitman/tests/test_git.py +++ b/gitman/tests/test_git.py @@ -1,7 +1,8 @@ # pylint: disable=no-self-use -from unittest.mock import patch, Mock import os +from unittest.mock import Mock, patch + from gitman import git from gitman.exceptions import ShellError diff --git a/gitman/tests/test_models_config.py b/gitman/tests/test_models_config.py index fc0c2032..61ef8671 100644 --- a/gitman/tests/test_models_config.py +++ b/gitman/tests/test_models_config.py @@ -1,6 +1,7 @@ -# pylint: disable=no-self-use,redefined-outer-name,unused-variable,expression-not-assigned,misplaced-comparison-constant +# pylint: disable=no-self-use,redefined-outer-name,unused-variable,expression-not-assigned,misplaced-comparison-constant,len-as-condition import os + import pytest from expecter import expect @@ -73,7 +74,7 @@ def test_install_with_dirs(self): assert 2 == count def test_install_with_dirs_unknown(self): - """Verify zero dependencies are installed with an unknown dependency.""" + """Verify zero dependencies are installed with unknown dependency.""" config = Config(FILES) count = config.install_dependencies('foobar') diff --git a/gitman/tests/test_models_source.py b/gitman/tests/test_models_source.py index 5e8f3e17..62216fbd 100644 --- a/gitman/tests/test_models_source.py +++ b/gitman/tests/test_models_source.py @@ -1,7 +1,7 @@ # pylint: disable=no-self-use,redefined-outer-name,misplaced-comparison-constant -from unittest.mock import patch, Mock from copy import copy +from unittest.mock import Mock, patch import pytest diff --git a/gitman/tests/test_plugin.py b/gitman/tests/test_plugin.py index 0e39f0b7..d1f6c422 100644 --- a/gitman/tests/test_plugin.py +++ b/gitman/tests/test_plugin.py @@ -1,6 +1,6 @@ # pylint: disable=no-self-use -from unittest.mock import patch, call +from unittest.mock import call, patch from gitman import plugin @@ -53,7 +53,7 @@ def test_update_no_lock(self, mock_commands): """Verify 'update' can be called without locking.""" mock_commands.update.__name__ = 'mock' - plugin.main(['--update', '--no-lock']) + plugin.main(['--update', '--skip-lock']) assert [ call.update(root=None, depth=None, @@ -82,6 +82,6 @@ def test_uninstall(self, mock_commands): plugin.main(['--uninstall', '--force']) assert [ - call.delete(root=None, force=True), + call.delete(root=None, force=True, keep_location=False), call.delete().__bool__(), # command status check ] == mock_commands.mock_calls diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index ef81111d..4c2acfd8 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -1,8 +1,7 @@ # pylint: disable=no-self-use,misplaced-comparison-constant,expression-not-assigned import os - -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from expecter import expect diff --git a/gitman/tests/test_system.py b/gitman/tests/test_system.py index 196cffad..53f23b9f 100644 --- a/gitman/tests/test_system.py +++ b/gitman/tests/test_system.py @@ -1,6 +1,6 @@ # pylint: disable=unused-variable,expression-not-assigned -from unittest.mock import patch, call, Mock +from unittest.mock import Mock, call, patch from expecter import expect diff --git a/mkdocs.yml b/mkdocs.yml index 442d53d0..58f6c39a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,9 @@ site_name: GitMan -site_description: A language-agnostic "dependency manager" using Git. +site_description: A language-agnostic dependency manager using Git. site_author: Jace Browning repo_url: https://github.com/jacebrowning/gitman -theme: mkdocs +theme: readthedocs pages: - Home: index.md @@ -19,6 +19,7 @@ pages: - Tracking Branches: use-cases/branch-tracking.md - Linking Feature Branches: use-cases/linked-features.md - Build System Integration: use-cases/build-integration.md + - Sparse Checkouts: use-cases/sparse-checkouts.md - About: - Release Notes: about/changelog.md - Contributing: about/contributing.md diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..b7fcfae1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,18 @@ +[pytest] + +addopts = + --strict + --pdbcls=tests:Debugger + + --verbose --verbose + -r sxX + + --cov=gitman + --cov-report=html + --cov-report=term-missing:skip-covered + --no-cov-on-fail + +cache_dir = .cache + +markers = + integration diff --git a/requirements/ci.txt b/requirements/ci.txt deleted file mode 100644 index 6b480628..00000000 --- a/requirements/ci.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Linters -pycodestyle -pydocstyle -pylint - -# Testing -pytest -pytest-describe -pytest-expecter -pytest-cov -pytest-random -freezegun - -# Coverage -coverage -coverage.space diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index de87427d..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Documentation -docutils -readme -pdoc -mkdocs -pygments - -# Tooling -sniffer - -# Runner -honcho - -# Build -wheel -pyinstaller - -# Release -twine diff --git a/scent.py b/scent.py index cd34d8e1..ab6b337b 100644 --- a/scent.py +++ b/scent.py @@ -1,11 +1,12 @@ """Configuration file for sniffer.""" -# pylint: disable=superfluous-parens,bad-continuation +# pylint: disable=superfluous-parens,bad-continuation,unpacking-non-sequence -import os -import time import subprocess +import time + +from sniffer.api import file_validator, runnable, select_runnable + -from sniffer.api import select_runnable, file_validator, runnable try: from pync import Notifier except ImportError: @@ -17,66 +18,69 @@ watch_paths = ["gitman", "tests"] -@select_runnable('python') +class Options: + 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', 'docs'), 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-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 @@ -84,14 +88,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 c3ccb001..8b996779 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -"""Setup script for the package.""" - import os import sys @@ -9,13 +7,13 @@ PACKAGE_NAME = 'gitman' -MINIMUM_PYTHON_VERSION = 3, 5 +MINIMUM_PYTHON_VERSION = '3.5' def check_python_version(): """Exit when the Python version is too low.""" - if sys.version_info < MINIMUM_PYTHON_VERSION: - sys.exit("Python {0}.{1}+ is required.".format(*MINIMUM_PYTHON_VERSION)) + if sys.version < MINIMUM_PYTHON_VERSION: + sys.exit("Python {0}+ is required.".format(MINIMUM_PYTHON_VERSION)) def read_package_variable(key, filename='__init__.py'): @@ -26,18 +24,14 @@ def read_package_variable(key, filename='__init__.py'): parts = line.strip().split(' ', 2) if parts[:-1] == [key, '=']: return parts[-1].strip("'") - sys.exit("'{0}' not found in '{1}'".format(key, module_path)) + sys.exit("'%s' not found in '%s'", key, module_path) -def read_descriptions(): +def build_description(): """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 + readme = open("README.md").read() + changelog = open("CHANGELOG.md").read() + return readme + '\n' + changelog check_python_version() @@ -47,7 +41,7 @@ def read_descriptions(): version=read_package_variable('__version__'), description=read_package_variable('DESCRIPTION'), - url='https://jacebrowning/gitman', + url='https://github.com/jacebrowning/gitman', author='Jace Browning', author_email='jacebrowning@gmail.com', @@ -60,7 +54,8 @@ def read_descriptions(): 'gdm = gitman.cli:main', ]}, - long_description=read_descriptions(), + long_description=build_description(), + long_description_content_type='text/markdown', license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -73,6 +68,7 @@ def read_descriptions(): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Software Development', 'Topic :: Software Development :: Build Tools', 'Topic :: Software Development :: Version Control', diff --git a/tests/__init__.py b/tests/__init__.py index a916f18f..96f579f0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,6 @@ """Integration tests for the package.""" + +try: + from IPython.terminal.debugger import TerminalPdb as Debugger +except ImportError: + from pdb import Pdb as Debugger diff --git a/tests/conftest.py b/tests/conftest.py index 0f74f413..c0be2394 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ """Integration tests configuration file.""" +# pylint: disable=unused-import + import yorm -from gitman.tests.conftest import pytest_configure # pylint: disable=unused-import +from gitman.tests.conftest import pytest_configure def pytest_runtest_setup(item): # pylint: disable=unused-argument diff --git a/tests/test_api.py b/tests/test_api.py index 82e4e4c2..1735e2ab 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,9 @@ -# pylint: disable=redefined-outer-name,unused-argument,unused-variable,singleton-comparison,expression-not-assigned +# pylint: disable=redefined-outer-name,unused-argument,unused-variable,singleton-comparison,expression-not-assigned,no-member +import logging import os import shutil from contextlib import suppress -import logging import pytest from expecter import expect @@ -11,8 +11,8 @@ import gitman from gitman import shell +from gitman.exceptions import InvalidRepository, UncommittedChanges from gitman.models import Config -from gitman.exceptions import UncommittedChanges, InvalidRepository from .utilities import strip @@ -25,18 +25,24 @@ sources: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-branch link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-tag link: scripts: - - name: gitman_3 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 9bf18e16b956041f0267c21baad555a23237b52e link: scripts: @@ -79,6 +85,8 @@ def it_creates_a_new_config_file(tmpdir): sources: - name: sample_dependency repo: https://github.com/githubtraining/hellogitworld + sparse_paths: + - rev: master link: scripts: @@ -86,6 +94,8 @@ def it_creates_a_new_config_file(tmpdir): sources_locked: - name: sample_dependency repo: https://github.com/githubtraining/hellogitworld + sparse_paths: + - rev: ebbbf773431ba07510251bb03f9525c7bab2b13a link: scripts: @@ -235,6 +245,35 @@ def it_detects_failures_in_scripts(config_with_scripts): def script_failures_can_be_ignored(config_with_scripts): expect(gitman.install(force=True)) == True + def describe_sparse_paths(): + @pytest.fixture + def config_with_scripts(config): + config.__mapper__.text = strip(""" + location: deps + sources: + - name: gitman_1 + repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - src/* + rev: ddbe17ef173538d1fda29bd99a14bab3c5d86e78 + link: + scripts: + - + """) + + return config + + def it_successfully_materializes_the_repo(config): + expect(gitman.install(depth=1, force=True)) == True + dir_listing = os.listdir(os.path.join(config.location, "gitman_1")) + expect(dir_listing).contains('src') + + def it_contains_only_the_sparse_paths(config): + expect(gitman.install(depth=1, force=True)) == True + dir_listing = os.listdir(os.path.join(config.location, "gitman_1")) + expect(dir_listing).contains('src') + expect(len(dir_listing) == 1) + def describe_uninstall(): @@ -260,6 +299,39 @@ def it_deletes_the_log_file(config): expect(os.path.exists(config.log_path)) == False +def describe_keep_location(): + + def it_deletes_dependencies_when_they_exist(config): + gitman.install('gitman_1', depth=1) + expect(os.path.isdir(config.location)) == True + + expect(gitman.uninstall(keep_location=True)) == True + + path = os.path.join(config.location, 'gitman_1') + expect(os.path.exists(path)) == False + + expect(os.path.exists(config.location)) == True + + gitman.uninstall() + + def it_should_not_fail_when_no_dependencies_exist(config): + expect(os.path.isdir(config.location)) == False + + expect(gitman.uninstall(keep_location=True)) == True + + gitman.uninstall() + + def it_deletes_the_log_file(config): + gitman.install('gitman_1', depth=1) + gitman.list() + expect(os.path.exists(config.log_path)) == True + + gitman.uninstall(keep_location=True) + expect(os.path.exists(config.log_path)) == False + + gitman.uninstall() + + def describe_update(): def it_should_not_modify_config(config): @@ -273,12 +345,16 @@ def it_locks_previously_locked_dependnecies(config): sources: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-branch link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-tag link: scripts: @@ -286,6 +362,8 @@ def it_locks_previously_locked_dependnecies(config): sources_locked: - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: (old revision) link: scripts: @@ -299,12 +377,16 @@ def it_locks_previously_locked_dependnecies(config): sources: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-branch link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-tag link: scripts: @@ -312,6 +394,8 @@ def it_locks_previously_locked_dependnecies(config): sources_locked: - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 7bd138fe7359561a8c2ff9d195dff238794ccc04 link: scripts: @@ -324,12 +408,16 @@ def it_should_not_lock_dependnecies_when_disabled(config): sources: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-branch link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-tag link: scripts: @@ -337,6 +425,8 @@ def it_should_not_lock_dependnecies_when_disabled(config): sources_locked: - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: (old revision) link: scripts: @@ -350,12 +440,16 @@ def it_should_not_lock_dependnecies_when_disabled(config): sources: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-branch link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: example-tag link: scripts: @@ -363,6 +457,8 @@ def it_should_not_lock_dependnecies_when_disabled(config): sources_locked: - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: (old revision) link: scripts: @@ -376,18 +472,24 @@ def it_should_lock_all_dependencies_when_enabled(config): sources_locked: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 1de84ca1d315f81b035cd7b0ecf87ca2025cdacd link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 7bd138fe7359561a8c2ff9d195dff238794ccc04 link: scripts: - - name: gitman_3 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 9bf18e16b956041f0267c21baad555a23237b52e link: scripts: @@ -426,18 +528,24 @@ def it_records_all_versions_when_no_arguments(config): sources_locked: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 1de84ca1d315f81b035cd7b0ecf87ca2025cdacd link: scripts: - - name: gitman_2 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 7bd138fe7359561a8c2ff9d195dff238794ccc04 link: scripts: - - name: gitman_3 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 9bf18e16b956041f0267c21baad555a23237b52e link: scripts: @@ -452,12 +560,16 @@ def it_records_specified_dependencies(config): sources_locked: - name: gitman_1 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 1de84ca1d315f81b035cd7b0ecf87ca2025cdacd link: scripts: - - name: gitman_3 repo: https://github.com/jacebrowning/gitman-demo + sparse_paths: + - rev: 9bf18e16b956041f0267c21baad555a23237b52e link: scripts: diff --git a/tests/test_cli.py b/tests/test_cli.py index fd9e2fd9..64c20ee4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable,redefined-outer-name,expression-not-assigned import os -from unittest.mock import patch, call +from unittest.mock import call, patch import pytest from expecter import expect diff --git a/tests/test_main.py b/tests/test_main.py index 6fa878da..a8624a84 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable,expression-not-assigned -import sys import subprocess +import sys from expecter import expect