diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml deleted file mode 100644 index 2cdcdf05..00000000 --- a/.github/workflows/flake8.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: flake8 lint -on: - push: - pull_request: - -jobs: - flake8-lint: - runs-on: ubuntu-24.04 - name: flake8 lint - steps: - - name: Setup python for flake8 - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - uses: actions/checkout@v3 - - name: Install nox - run: python -m pip install nox==2024.10.09 - - name: Setup flake8 - run: nox --force-color -e flake8 --install-only - - name: Run flake8 - run: nox --force-color -e flake8 -vv diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c70f1ed5..38c2d99b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,15 +6,18 @@ on: schedule: - cron: "0 8 * * *" +permissions: + contents: read + jobs: pre-commit: name: pre-commit runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install dependencies @@ -28,36 +31,42 @@ jobs: test: name: test ${{ matrix.py }} - ${{ matrix.netapi }} - ${{ matrix.salt }} runs-on: ubuntu-22.04 + timeout-minutes: 30 strategy: fail-fast: false matrix: py: - - "3.8" - - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" netapi: - "cherrypy" - "tornado" salt: - - "v3006.9" - - "v3007.1" + - "v3006.30" + - "v3007.17" + - "v3008.5" - "master" exclude: - - py: "3.8" - salt: "v3007.1" - - py: "3.9" - salt: "v3007.1" - - py: "3.8" - salt: "master" - - py: "3.9" - salt: "master" + # The test minion never completes authentication with Salt 3006 on + # Python 3.12+ or Salt 3007 on Python 3.13. Salt 3007 on Python 3.12 + # usually passes, but the minion intermittently fails to start or + # stops answering. + - py: "3.12" + salt: "v3006.30" + - py: "3.13" + salt: "v3006.30" + - py: "3.12" + salt: "v3007.17" + - py: "3.13" + salt: "v3007.17" env: SALT_REQUIREMENT: ${{ matrix.salt == 'master' && 'salt@git+https://github.com/saltstack/salt.git@master' || format('salt=={0}', matrix.salt) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up Python ${{ matrix.py }} - uses: actions/setup-python@v4 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.py }} diff --git a/.gitignore b/.gitignore index be26a46b..1de16c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist/ salt_pepper.egg-info/ .nox/ artifacts/ +tests/.pepperrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d13dd7b..846c056f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,47 @@ --- minimum_pre_commit_version: 3.5.0 repos: + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-merge-conflict # Check for files that contain merge conflict strings. - id: trailing-whitespace # Trims trailing whitespace. - args: [--markdown-linebreak-ext=md] + args: + - --markdown-linebreak-ext=md - id: mixed-line-ending # Replaces or checks mixed line ending. - args: [--fix=lf] + args: + - --fix=lf - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. - - id: check-merge-conflict # Check for files that contain merge conflict strings. - id: check-ast # Simply check whether files parse as valid python. + - id: check-case-conflict # Check for files with names that would conflict on a + # case-insensitive filesystem like MacOS HFS+ or Windows FAT. + - id: check-json # Attempts to load all json files to verify syntax. + - id: check-symlinks # Checks for symlinks which do not point to anything. + - id: debug-statements # Check for debugger imports and breakpoint() calls in python source. + - id: fix-byte-order-marker # removes UTF-8 byte order marker + - id: forbid-submodules # forbids any submodules in the repository. + - id: fix-encoding-pragma # Remove `# -*- coding: utf-8 -*-` from the top of python files. + args: + - --remove - # ----- Formatting ----------------------------------------------------------------------------> - - repo: https://github.com/myint/autoflake - rev: v2.2.1 + - repo: https://github.com/adrienverge/yamllint + rev: v1.38.0 hooks: - - id: autoflake - name: Remove unused variables and imports - language: python - args: ["--in-place", "--remove-all-unused-imports", "--remove-unused-variables", "--expand-star-imports"] - files: \.py$ + - id: yamllint + name: Check for duplicate keys in pre-commit config + files: ^\.pre-commit-config\.yaml$ + args: + - -d + - "{rules: {key-duplicates: enable}}" + + # ----- Code Formatting -------------------------------------------------------------------------------------------> + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + name: Upgrade code to Py3.10+ + args: [--py310-plus, --keep-mock] - repo: https://github.com/saltstack/pre-commit-remove-import-headers rev: 1.1.0 @@ -33,32 +53,70 @@ repos: rev: 2.5.2 hooks: - id: salt-rewrite - alias: rewrite-tests - name: Rewrite the test suite - files: ^tests/.*\.py$ - args: [--silent, -E, fix_docstrings] - - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 - hooks: - - id: pyupgrade - name: Rewrite Code to be Py3+ - args: [ - --py3-plus - ] + alias: rewrite-docstrings + name: Pepper docstrings auto-fixes + files: ^pepper/.*\.py$ + args: [--silent, -F, fix_docstrings] - - repo: https://github.com/asottile/reorder_python_imports - rev: v3.12.0 + - repo: https://github.com/timothycrosley/isort + rev: 5.13.2 hooks: - - id: reorder-python-imports - args: [ - --py3-plus, - ] + - id: isort + additional_dependencies: ['toml'] - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.2.0 hooks: - id: black - args: [-l 100] - # <---- Formatting ----------------------------------------------------------------------------- + - repo: https://github.com/asottile/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + args: [--skip-errors] + files: ^README\.rst$ + additional_dependencies: + - black==24.2.0 + # <---- Code Formatting -------------------------------------------------------------------------------------------- + + # ----- Security --------------------------------------------------------------------------------------------------> + - repo: https://github.com/PyCQA/bandit + rev: "1.7.7" + hooks: + - id: bandit + alias: bandit-pepper + name: Run bandit against Pepper + args: [--silent, -lll, --skip, "B701,B324"] + exclude: ^tests/.* + additional_dependencies: ['importlib_metadata<5'] + - repo: https://github.com/PyCQA/bandit + rev: "1.7.7" + hooks: + - id: bandit + alias: bandit-tests + name: Run bandit against the test suite + args: [--silent, -lll, --skip, "B701,B324"] + files: ^tests/.* + additional_dependencies: ['importlib_metadata<5'] + # <---- Security --------------------------------------------------------------------------------------------------- + + # ----- Lint ------------------------------------------------------------------------------------------------------> + - repo: local + hooks: + - id: lint-pepper + name: Lint Pepper + entry: pylint --rcfile=.pylintrc --disable=I + language: python + files: ^((setup|noxfile)\.py|pepper/.*\.py)$ + additional_dependencies: &lint_deps + - pylint==3.1.1 + - astroid==3.1.0 + - saltpylint==2024.2.5 + + - id: lint-tests + name: Lint Tests + entry: pylint --rcfile=.pylintrc --disable=I + language: python + files: ^tests/.*\.py$ + additional_dependencies: *lint_deps + # <---- Lint ------------------------------------------------------------------------------------------------------- diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..980dead0 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,701 @@ +[MAIN] + +# 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 + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# 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-allow-list= + +# 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. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# 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= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins=saltpylint.fileperms, + saltpylint.smartup, + saltpylint.thirdparty, + saltpylint.dunder_del + +# Pickle collected data for later comparisons. +persistent=no + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=exc, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# 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. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# 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] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=35 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=48 + +# Maximum number of locals for function / method body. +max-locals=40 + +# 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=100 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# 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=120 + +# Maximum number of lines in a module. +max-module-lines=3000 + +# 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 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to 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 + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# 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 re-enable 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=R, + deprecated-pragma, + file-ignored, + locally-disabled, + suppressed-message, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + use-implicit-booleaness-not-comparison, + use-symbolic-message-instead, + useless-suppression, + 3rd-party-local-module-not-gated, + attribute-defined-outside-init, + bad-continuation, + bad-mcs-classmethod-argument, + bad-mcs-method-argument, + blacklisted-name, + deprecated-method, + empty-docstring, + exec-used, + file-ignored, + fixme, + global-statement, + import-error, + import-outside-toplevel, + invalid-format-index, + invalid-name, + keyword-arg-before-vararg, + line-too-long, + locally-disabled, + logging-format-interpolation, + logging-fstring-interpolation, + missing-docstring, + no-member, + protected-access, + raw-checker-failed, + redefined-builtin, + redefined-outer-name, + reimported, + too-many-lines, + undefined-loop-variable, + unexpected-special-method-signature, + ungrouped-imports, + unidiomatic-typecheck, + unsubscriptable-object, + unused-argument, + unused-variable, + wrong-import-order, + wrong-import-position, + consider-using-f-string, + raise-missing-from, + broad-exception-raised, + consider-using-dict-items + +# 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= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 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, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=parseable + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=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 to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=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 symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# 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,argparse.Namespace + +# 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 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +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,past.builtins,future.builtins,builtins,io + + +[FILEPERMS] + +# Desired file permissons. Default: 0644 +fileperms-default=0644 + +# File paths to ignore file permission. Glob patterns allowed. +fileperms-ignore-paths=setup.py,noxfile.py,scripts/pepper + +[3RD-PARTY-IMPORTS] + +# Known 3rd-party modules which don' require being gated, separated by a comma +allowed-3rd-party-modules=requests, + requests_gssapi, + pytest, + _pytest, + saltfactories, + pytestshellutils, + pytestskipmarkers, + nox, + setuptools, + salt, + pepper, + tests diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dbdda311..00000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -dist: xenial -sudo: false -services: -- docker -language: python -services: -- docker - -install: -- pip install tox - -python: -- '2.7' -- '3.4' -- '3.5' -- '3.6' -- '3.7' -- '3.8-dev' - -env: -- SALT=v2018.3 BACKEND=cherrypy -- SALT=v2018.3 BACKEND=tornado -- SALT=v2019.2 BACKEND=cherrypy -- SALT=v2019.2 BACKEND=tornado -- SALT=develop BACKEND=cherrypy -- SALT=develop BACKEND=tornado - -matrix: - allow_failures: - - python: '3.8-dev' - - env: SALT=develop BACKEND=tornado - - env: SALT=develop BACKEND=cherrypy - -script: -- PYTHON="${TRAVIS_PYTHON_VERSION/-dev/-rc}" -- TOX_VERSION="py${PYTHON//./}" -- docker run -v $PWD:/pepper -ti --rm "python:$PYTHON" make -C /pepper test PYVERSION="${TOX_VERSION%-rc}" SALT="${SALT}" BACKEND="${BACKEND}" - -after_success: -- sudo chown $USER .tox/ -- tox -e codecov diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..435adc17 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +The history before this file was started is in the git tags. diff --git a/Makefile b/Makefile deleted file mode 100644 index 1c69fd34..00000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -PYVERSION?=py37 -SALT?=v2019.2 -BACKEND?=cherrypy -DEBIAN_FRONTEND=noninteractive - -install: - apt update && apt install -y libc6-dev libffi-dev gcc git openssh-server libzmq3-dev - pip install tox - -test: install - tox -e flake8,$(PYVERSION)-$(BACKEND)-$(SALT) diff --git a/README.rst b/README.rst index 6b285a32..ccd3a254 100644 --- a/README.rst +++ b/README.rst @@ -4,14 +4,12 @@ Pepper .. image:: https://img.shields.io/pypi/v/salt-pepper.svg :target: https://pypi.org/project/salt-pepper -.. image:: https://travis-ci.com/saltstack/pepper.svg?branch=develop - :target: https://travis-ci.com/saltstack/pepper +.. image:: https://github.com/saltstack/pepper/actions/workflows/test.yaml/badge.svg?branch=develop + :target: https://github.com/saltstack/pepper/actions/workflows/test.yaml .. image:: https://img.shields.io/pypi/pyversions/salt-pepper.svg :target: https://pypi.org/project/salt-pepper .. image:: https://img.shields.io/badge/license-Apache2-blue.svg?maxAge=3600 :target: https://pypi.org/project/salt-pepper -.. image:: https://codecov.io/gh/saltstack/pepper/branch/develop/graph/badge.svg - :target: https://codecov.io/gh/saltstack/pepper/branch/develop Pepper contains a Python library and CLI scripts for accessing a remote `salt-api`__ instance. @@ -73,11 +71,32 @@ or in a configuration file ``$HOME/.pepperrc`` with the following syntax : Contributing ------------ -Please feel free to get involved by sending pull requests or join us on the -Salt mailing list or on IRC in #salt or #salt-devel. +Pull requests are welcome. This repo follows the same `contributing +guidelines`__ as Salt. Open pull requests against the ``develop`` branch. -This repo follows the same `contributing guidelines`__ as Salt and uses -separate develop and master branches for in-progress additions and bug-fix -changes respectively. +The pre-commit hooks run the same formatters and linters as Salt (black, +isort, pyupgrade, bandit and pylint): -.. __: https://docs.saltstack.com/en/latest/topics/development/contributing.html +.. code-block:: bash + + pip install pre-commit + pre-commit install + +Changes that affect users need a changelog fragment in the ``changelog/`` +directory, named ``..md``, where the type is one of +``removed``, ``deprecated``, ``changed``, ``fixed``, ``added`` or +``security``: + +.. code-block:: bash + + echo "Fixed the exit code when a runner job fails" > changelog/195.fixed.md + +The test suite starts a salt-master, salt-minion and salt-api, and runs under +nox: + +.. code-block:: bash + + pip install nox + nox -e tests + +.. __: https://docs.saltproject.io/en/latest/topics/development/contributing.html diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 87d32eb4..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,60 +0,0 @@ -trigger: -- develop - -variables: - python: '["py2.7", "py3.4", "py3.5", "py3.6", "py3.7", "py3.8"]' - salt: '["v2018.3", "v2019.2", "develop"]' - backends: '["cherrypy", "tornado"]' - -jobs: -- job: build_matrix - pool: - vmImage: 'Ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.7' - architecture: x64 - - - task: PythonScript@0 - name: matrix - inputs: - scriptSource: Inline - script: | - import json - matrix = {} - for pyver in $(python): - for saltver in $(salt): - for backend in $(backends): - matrix['{0}-{1}-{2}'.format(pyver, backend, saltver)] = { - 'python.version': pyver[2:], - 'version': pyver.replace('.', ''), - 'salt': saltver, - 'backend': backend, - } - print('##vso[task.setvariable variable=matrix;isOutput=true]{0}'.format(json.dumps(matrix))) - -- job: test_pepper - dependsOn: build_matrix - pool: - vmImage: 'Ubuntu-16.04' - strategy: - matrix: $[ dependencies.build_matrix.outputs['matrix.matrix'] ] - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: x64 - - - script: | - pip install tox - displayName: Install dependencies - - script: | - docker run -v $PWD:/pepper --rm "python:$(python.version)" make -C /pepper test PYTHON_VERSION=$(version) SALT=$(salt) BACKEND=$(backend) - displayName: pytest - - script: | - sudo chown $USER .tox/ - tox -e codecov - displayName: codecov - env: - CODECOV_TOKEN: '16c2a232-4329-438c-b163-ccbfeeab47aa' diff --git a/changelog/.template.jinja b/changelog/.template.jinja new file mode 100644 index 00000000..22db30f5 --- /dev/null +++ b/changelog/.template.jinja @@ -0,0 +1,15 @@ +{% if sections[""] %} +{% for category, val in definitions.items() if category in sections[""] %} + +### {{ definitions[category]['name'] }} + +{% for text, values in sections[""][category].items() %} +- {{ text | indent(2) }} {{ values|join(', ') }} +{% endfor %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} diff --git a/changelog/206.added.md b/changelog/206.added.md new file mode 100644 index 00000000..7475f0c1 --- /dev/null +++ b/changelog/206.added.md @@ -0,0 +1 @@ +Added a changelog, generated with towncrier from fragments in the changelog/ directory. diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 2ef3d9f3..00000000 --- a/codecov.yml +++ /dev/null @@ -1,27 +0,0 @@ -codecov: - notify: - require_ci_to_pass: yes - branch: develop - -coverage: - precision: 2 - round: down - range: "50...100" - - status: - project: yes - patch: yes - changes: no - -parsers: - gcov: - branch_detection: - conditional: yes - loop: yes - method: no - macro: no - -comment: - layout: "header, diff" - behavior: default - require_changes: no diff --git a/noxfile.py b/noxfile.py index 432929d4..4605befc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,9 +6,8 @@ import nox from nox.command import CommandFailed - -API_BACKEND = [os.environ.get("API_BACKEND")] or ["cherrypy", "tornado"] -PYTHON_VERSIONS = [os.environ.get("PYTHON_VERSIONS")] or ["3.8", "3.9", "3.10", "3.11"] +API_BACKEND = os.environ.get("API_BACKEND", "cherrypy tornado").split() +PYTHON_VERSIONS = os.environ.get("PYTHON_VERSIONS", "3.10 3.11 3.12 3.13").split() # Nox options # Reuse existing virtualenvs @@ -18,11 +17,11 @@ # Be verbose when running under a CI context CI_RUN = os.environ.get("CI") -PIP_INSTALL_SILENT = CI_RUN is False +PIP_INSTALL_SILENT = not CI_RUN SKIP_REQUIREMENTS_INSTALL = "SKIP_REQUIREMENTS_INSTALL" in os.environ EXTRA_REQUIREMENTS_INSTALL = os.environ.get("EXTRA_REQUIREMENTS_INSTALL") SALT_REQUIREMENT = os.environ.get("SALT_REQUIREMENT") or "salt" -COVERAGE_VERSION_REQUIREMENT = "coverage==5.5" +COVERAGE_VERSION_REQUIREMENT = "coverage>=7.2" # Prevent Python from writing bytecode os.environ["PYTHONDONTWRITEBYTECODE"] = "1" @@ -59,11 +58,15 @@ def _install_requirements( session.install("--progress-bar=off", "wheel", silent=PIP_INSTALL_SILENT) if install_coverage_requirements: session.install( - "--progress-bar=off", COVERAGE_VERSION_REQUIREMENT, silent=PIP_INSTALL_SILENT + "--progress-bar=off", + COVERAGE_VERSION_REQUIREMENT, + silent=PIP_INSTALL_SILENT, ) # Install the latest salt package if install_salt: - session.install("--progress-bar=off", SALT_REQUIREMENT, silent=PIP_INSTALL_SILENT) + session.install( + "--progress-bar=off", SALT_REQUIREMENT, silent=PIP_INSTALL_SILENT + ) if install_test_requirements: requirements_file = REPO_ROOT / "tests" / "requirements.txt" @@ -82,7 +85,9 @@ def _install_requirements( EXTRA_REQUIREMENTS_INSTALL, ) install_command = ["--progress-bar=off"] - install_command += [req.strip() for req in EXTRA_REQUIREMENTS_INSTALL.split()] + install_command += [ + req.strip() for req in EXTRA_REQUIREMENTS_INSTALL.split() + ] session.install(*install_command, silent=PIP_INSTALL_SILENT) if install_source: @@ -95,14 +100,12 @@ def _install_requirements( session.install(pkg, silent=PIP_INSTALL_SILENT) -""" -Nox session to run tests for corresponding python versions and salt versions -""" - - @nox.session(python=PYTHON_VERSIONS) @nox.parametrize("api_backend", API_BACKEND) def tests(session, api_backend): + """ + Run the test suite against a salt-api backend + """ _install_requirements(session) args = [ @@ -140,7 +143,12 @@ def tests(session, api_backend): args.append("tests/") try: session.run( - "coverage", "run", "-m", "pytest", f"--salt-api-backend=rest_{api_backend}", *args + "coverage", + "run", + "-m", + "pytest", + f"--salt-api-backend=rest_{api_backend}", + *args, ) finally: # Always combine and generate the XML coverage report @@ -163,13 +171,7 @@ def tests(session, api_backend): finally: # Move the coverage DB to artifacts/coverage in order for it to be archived by CI if COVERAGE_REPORT_DB.exists(): - shutil.move(str(COVERAGE_REPORT_DB), str(ARTIFACTS_DIR / COVERAGE_REPORT_DB.name)) - - -@nox.session(python="3.10") -def flake8(session): - _install_requirements(session) - # Install flake8 - session.install("flake8") - # Run flake8 - session.run("flake8", "tests/", "pepper/", "scripts/pepper", "setup.py") + shutil.move( + str(COVERAGE_REPORT_DB), + str(ARTIFACTS_DIR / COVERAGE_REPORT_DB.name), + ) diff --git a/pepper/__init__.py b/pepper/__init__.py index a6ece354..ad55c3df 100644 --- a/pepper/__init__.py +++ b/pepper/__init__.py @@ -1,11 +1,10 @@ """ Pepper is a CLI front-end to salt-api """ -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version -from pepper.libpepper import Pepper -from pepper.libpepper import PepperException +from importlib.metadata import PackageNotFoundError, version + +from pepper.libpepper import Pepper, PepperException __all__ = ("__version__", "Pepper", "PepperException") diff --git a/pepper/cli.py b/pepper/cli.py index fed02587..65bd0a16 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -2,44 +2,23 @@ A CLI interface to a remote salt-api instance """ + import getpass import json import logging -import optparse +import optparse # pylint: disable=deprecated-module import os -import sys import textwrap import time +from configparser import ConfigParser +from json import JSONDecodeError import pepper -from pepper.exceptions import PepperArgumentsException -from pepper.exceptions import PepperAuthException -from pepper.exceptions import PepperException - -# Import Pepper Libraries - - -try: - # Python 3 - from configparser import ConfigParser, RawConfigParser -except ImportError: - # Python 2 - from ConfigParser import ConfigParser, RawConfigParser - -try: - # Python 3 - JSONDecodeError = json.decode.JSONDecodeError -except AttributeError: - # Python 2 - JSONDecodeError = ValueError - -try: - input = raw_input -except NameError: - pass - -if sys.version_info[0] == 2: - FileNotFoundError = IOError +from pepper.exceptions import ( + PepperArgumentsException, + PepperAuthException, + PepperException, +) logger = logging.getLogger(__name__) @@ -70,7 +49,9 @@ def parse(self): self.parser.add_option( "-c", dest="config", - default=os.environ.get("PEPPERRC", os.path.join(os.path.expanduser("~"), ".pepperrc")), + default=os.environ.get( + "PEPPERRC", os.path.join(os.path.expanduser("~"), ".pepperrc") + ), help=textwrap.dedent( """ Configuration file location. Default is a file path in the @@ -472,7 +453,9 @@ def add_retcodeopts(self): ret code validation options """ optgroup = optparse.OptionGroup( - self.parser, "retcode Field Validation Options", "Validate return.HOST.retcode fields" + self.parser, + "retcode Field Validation Options", + "Validate return.HOST.retcode fields", ) optgroup.add_option( @@ -520,10 +503,7 @@ def get_login_details(self): "SALTAPI_EAUTH": "auto", } - try: - config = ConfigParser(interpolation=None) - except TypeError: - config = RawConfigParser() + config = ConfigParser(interpolation=None) config.read(self.options.config) # read file @@ -573,10 +553,7 @@ def parse_url(self): """ url = "https://localhost:8000/" - try: - config = ConfigParser(interpolation=None) - except TypeError: - config = RawConfigParser() + config = ConfigParser(interpolation=None) config.read(self.options.config) # read file @@ -626,13 +603,15 @@ def parse_cmd(self, api): if self.options.json_file: try: - with open(self.options.json_file) as json_content: + with open(self.options.json_file, encoding="utf-8") as json_content: try: return json.load(json_content) - except JSONDecodeError: + except (JSONDecodeError, UnicodeDecodeError): raise PepperArgumentsException("Invalid JSON given.") except FileNotFoundError: - raise PepperArgumentsException("Cannot open file: %s", self.options.json_file) + raise PepperArgumentsException( + f"Cannot open file: {self.options.json_file}" + ) args = list(self.args) @@ -691,7 +670,7 @@ def parse_cmd(self, api): low["batch"] = self.options.batch low["arg"] = args else: - raise PepperException("Client not implemented: {}".format(client)) + raise PepperException(f"Client not implemented: {client}") return [low] @@ -758,24 +737,26 @@ def login(self, api): if self.options.mktoken: token_file = self.options.cache try: - with open(token_file) as f: + with open(token_file, encoding="utf-8") as f: auth = json.load(f) if auth["expire"] < time.time() + 30: logger.error("Login token expired") raise Exception("Login token expired") - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught if e.args[0] != 2: - logger.error("Unable to load login token from {} {}".format(token_file, str(e))) + logger.error( + f"Unable to load login token from {token_file} {str(e)}" + ) if os.path.isfile(token_file): os.remove(token_file) auth = login(**self.parse_login()) + oldumask = os.umask(0) try: - oldumask = os.umask(0) fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600) with os.fdopen(fdsc, "wt") as f: json.dump(auth, f) - except Exception as e: - logger.error("Unable to save token to {} {}".format(token_file, str(e))) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(f"Unable to save token to {token_file} {str(e)}") finally: os.umask(oldumask) else: diff --git a/pepper/libpepper.py b/pepper/libpepper.py index 7df31436..7541e0e5 100644 --- a/pepper/libpepper.py +++ b/pepper/libpepper.py @@ -4,6 +4,7 @@ (Specifically the rest_cherrypy netapi module.) """ + import json import logging import re @@ -13,32 +14,19 @@ try: ssl._create_default_https_context = ssl._create_stdlib_context -except Exception: +except Exception: # pylint: disable=broad-exception-caught pass -try: - from urllib.request import ( - HTTPHandler, - HTTPSHandler, - Request, - urlopen, - install_opener, - build_opener, - ) - from urllib.error import HTTPError, URLError - import urllib.parse as urlparse -except ImportError: - from urllib2 import ( - HTTPHandler, - HTTPSHandler, - Request, - urlopen, - install_opener, - build_opener, - HTTPError, - URLError, - ) - import urlparse +import urllib.parse as urlparse +from urllib.error import HTTPError, URLError +from urllib.request import ( + HTTPHandler, + HTTPSHandler, + Request, + build_opener, + install_opener, + urlopen, +) logger = logging.getLogger(__name__) @@ -72,23 +60,28 @@ class Pepper: """ - def __init__(self, api_url="https://localhost:8000", debug_http=False, ignore_ssl_errors=False): + def __init__( + self, + api_url="https://localhost:8000", + debug_http=False, + ignore_ssl_errors=False, + ): """ Initialize the class with the URL of the API :param api_url: Host or IP address of the salt-api URL; include the port number - :param debug_http: Add a flag to urllib2 to output the HTTP exchange + :param debug_http: Add a flag to urllib to output the HTTP exchange - :param ignore_ssl_errors: Add a flag to urllib2 to ignore invalid SSL certificates + :param ignore_ssl_errors: Add a flag to urllib to ignore invalid SSL certificates :raises PepperException: if the api_url is misformed """ split = urlparse.urlsplit(api_url) if split.scheme not in ["http", "https"]: - raise PepperException("salt-api URL missing HTTP(s) protocol: {}".format(api_url)) + raise PepperException(f"salt-api URL missing HTTP(s) protocol: {api_url}") self.api_url = api_url self.debug_http = int(debug_http) @@ -122,7 +115,6 @@ def req_stream(self, path): headers.setdefault("X-Auth-Token", self.auth["token"]) else: raise PepperException("Authentication required") - return params = { "url": self._construct_url(path), "headers": headers, @@ -134,15 +126,14 @@ def req_stream(self, path): if resp.status_code == 401: raise PepperException(str(resp.status_code) + ":Authentication denied") - return if resp.status_code == 500: raise PepperException(str(resp.status_code) + ":Server error.") - return if resp.status_code == 404: - raise PepperException(str(resp.status_code) + " :This request returns nothing.") - return + raise PepperException( + str(resp.status_code) + " :This request returns nothing." + ) except PepperException as e: print(e) return @@ -166,7 +157,6 @@ def req_get(self, path): headers.setdefault("X-Auth-Token", self.auth["token"]) else: raise PepperException("Authentication required") - return params = { "url": self._construct_url(path), "headers": headers, @@ -177,15 +167,14 @@ def req_get(self, path): if resp.status_code == 401: raise PepperException(str(resp.status_code) + ":Authentication denied") - return if resp.status_code == 500: raise PepperException(str(resp.status_code) + ":Server error.") - return if resp.status_code == 404: - raise PepperException(str(resp.status_code) + " :This request returns nothing.") - return + raise PepperException( + str(resp.status_code) + " :This request returns nothing." + ) except PepperException as e: print(e) return @@ -193,7 +182,7 @@ def req_get(self, path): def req(self, path, data=None): """ - A thin wrapper around urllib2 to send requests and return the response + A thin wrapper around urllib to send requests and return the response If the current instance contains an authentication token it will be attached to the request as a custom header. @@ -241,7 +230,7 @@ def req(self, path, data=None): # Send request try: - if not (self._ssl_verify): + if not self._ssl_verify: con = ssl.SSLContext(ssl.PROTOCOL_SSLv23) f = urlopen(req, context=con) else: @@ -264,7 +253,7 @@ def req(self, path, data=None): if status == 500: raise PepperException("Server error.") - logger.error("Error with request: {}".format(exc)) + logger.error(f"Error with request: {exc}") raise except AttributeError: logger.debug("Error converting response from JSON", exc_info=True) @@ -284,7 +273,7 @@ def req_requests(self, path, data=None): """ import requests - from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL + from requests_gssapi import OPTIONAL, HTTPSPNEGOAuth auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL) headers = { @@ -302,7 +291,7 @@ def req_requests(self, path, data=None): "auth": auth, "data": json.dumps(data), } - logger.debug("postdata {}".format(params)) + logger.debug(f"postdata {params}") resp = requests.post(**params) if resp.status_code == 401: # TODO should be resp.raise_from_status @@ -326,7 +315,9 @@ def low(self, lowstate, path="/"): """ return self.req(path, lowstate) - def local(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, ret=None): + def local( + self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, ret=None + ): """ Run a single command using the ``local`` client @@ -355,7 +346,9 @@ def local(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, return self.low([low]) - def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, ret=None): + def local_async( + self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, ret=None + ): """ Run a single command using the ``local_async`` client @@ -384,7 +377,9 @@ def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout= return self.low([low]) - def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", batch="50%", ret=None): + def local_batch( + self, tgt, fun, arg=None, kwarg=None, expr_form="glob", batch="50%", ret=None + ): """ Run a single command using the ``local_batch`` client @@ -420,7 +415,7 @@ def lookup_jid(self, jid): Wraps :meth:`runner`. """ - return self.runner("jobs.lookup_jid", jid="{}".format(jid)) + return self.runner("jobs.lookup_jid", jid=f"{jid}") def runner(self, fun, arg=None, **kwargs): """ diff --git a/pepper/retcode.py b/pepper/retcode.py index d8ce50eb..882c4eed 100644 --- a/pepper/retcode.py +++ b/pepper/retcode.py @@ -46,7 +46,8 @@ def validate_fail_any(result): retcodes = list( minion[name].get("retcode") for name in minion - if isinstance(minion[name], dict) and minion[name].get("retcode") is not None + if isinstance(minion[name], dict) + and minion[name].get("retcode") is not None ) return next((r for r in retcodes if r != 0), 0) return 0 @@ -68,7 +69,8 @@ def validate_fail_any_none(result): retcodes = list( minion[name].get("retcode") for name in minion - if isinstance(minion[name], dict) and minion[name].get("retcode") is not None + if isinstance(minion[name], dict) + and minion[name].get("retcode") is not None ) if not retcodes: return -1 # there are no retcodes @@ -92,7 +94,8 @@ def validate_fail_all(result): retcodes = list( minion[name].get("retcode") for name in minion - if isinstance(minion[name], dict) and minion[name].get("retcode") is not None + if isinstance(minion[name], dict) + and minion[name].get("retcode") is not None ) if all(r != 0 for r in retcodes): return next((r for r in retcodes if r != 0), 0) @@ -115,7 +118,8 @@ def validate_fail_all_none(result): retcodes = list( minion[name].get("retcode") for name in minion - if isinstance(minion[name], dict) and minion[name].get("retcode") is not None + if isinstance(minion[name], dict) + and minion[name].get("retcode") is not None ) if not retcodes: return -1 # there are no retcodes diff --git a/pepper/script.py b/pepper/script.py index 04293859..844bd574 100755 --- a/pepper/script.py +++ b/pepper/script.py @@ -7,14 +7,16 @@ import sys from pepper.cli import PepperCli -from pepper.exceptions import PepperArgumentsException -from pepper.exceptions import PepperAuthException -from pepper.exceptions import PepperException +from pepper.exceptions import ( + PepperArgumentsException, + PepperAuthException, + PepperException, +) from pepper.retcode import PepperRetcode try: - import salt.loader import salt.config + import salt.loader import salt.output HAS_SALT = True @@ -61,7 +63,10 @@ def __call__(self): for minionid, minionret in ret.items(): # rest_tornado doesnt return full_return directly # it will always be from get_event, so the output differs slightly - if isinstance(minionret, dict) and "return" in minionret: + if ( + isinstance(minionret, dict) + and "return" in minionret + ): # version >= 2017.7 salt.output.display_output( {minionid: minionret["return"]}, @@ -71,7 +76,10 @@ def __call__(self): opts=self.opts, ) # cherrypy returns with ret via full_return - elif isinstance(minionret, dict) and "ret" in minionret: + elif ( + isinstance(minionret, dict) + and "ret" in minionret + ): # version >= 2017.7 salt.output.display_output( {minionid: minionret["ret"]}, @@ -89,15 +97,21 @@ def __call__(self): elif "data" in ret: # unfold runners outputter = ret.get("outputter", "nested") - if isinstance(ret["data"], dict) and "return" in ret["data"]: + if ( + isinstance(ret["data"], dict) + and "return" in ret["data"] + ): ret = ret["data"]["return"] salt.output.display_output( - ret, self.cli.options.output or outputter, opts=self.opts + ret, + self.cli.options.output or outputter, + opts=self.opts, ) else: salt.output.display_output( {self.cli.options.client: ret}, - self.cli.options.output or ret.get("outputter", "nested"), + self.cli.options.output + or ret.get("outputter", "nested"), opts=self.opts, ) else: @@ -108,7 +122,9 @@ def __call__(self): ) else: if self.cli.options.output_file is not None: - with open(self.cli.options.output_file, "a") as ofile: + with open( + self.cli.options.output_file, "a", encoding="utf-8" + ) as ofile: print(result, file=ofile) else: print(result) @@ -117,12 +133,12 @@ def __call__(self): return PepperRetcode().validate(self.cli.options, result) return exit_code except (PepperException, PepperAuthException, PepperArgumentsException) as exc: - print("Pepper error: {}".format(exc), file=sys.stderr) + print(f"Pepper error: {exc}", file=sys.stderr) return 1 except KeyboardInterrupt: # TODO: mimic CLI and output JID on ctrl-c return 0 - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught print(e) print( "Uncaught Pepper error (increase verbosity for the full traceback).", diff --git a/pyproject.toml b/pyproject.toml index db3c13c5..d4cfb3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,5 +4,53 @@ testpaths = ["tests"] norecursedirs = [".git", ".nox"] usefixtures = ["pepperconfig"] -[tool.flake8] -max-line-length = 119 +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 +ensure_newline_before_comments=true +known_third_party = [ + "pytest" +] + +[tool.towncrier] + package = "pepper" + filename = "CHANGELOG.md" + directory = "changelog/" + template = "changelog/.template.jinja" + start_string = "# Changelog\n" + underlines = ["", "", ""] + title_format = "## {version} ({project_date})" + issue_format = "[#{issue}](https://github.com/saltstack/pepper/issues/{issue})" + + [[tool.towncrier.type]] + directory = "removed" + name = "Removed" + showcontent = true + + [[tool.towncrier.type]] + directory = "deprecated" + name = "Deprecated" + showcontent = true + + [[tool.towncrier.type]] + directory = "changed" + name = "Changed" + showcontent = true + + [[tool.towncrier.type]] + directory = "fixed" + name = "Fixed" + showcontent = true + + [[tool.towncrier.type]] + directory = "added" + name = "Added" + showcontent = true + + [[tool.towncrier.type]] + directory = "security" + name = "Security" + showcontent = true diff --git a/scripts/pepper b/scripts/pepper index 4ab005de..9e7eed3e 100644 --- a/scripts/pepper +++ b/scripts/pepper @@ -1,20 +1,12 @@ #!/usr/bin/env python -# Import Python Libraries import logging -# Import Pepper Libraries import pepper.script -try: - from logging import NullHandler -except ImportError: # Python < 2.7 - class NullHandler(logging.Handler): - def emit(self, record): pass - logging.basicConfig(format='%(levelname)s %(asctime)s %(module)s: %(message)s') logger = logging.getLogger('pepper') -logger.addHandler(NullHandler()) +logger.addHandler(logging.NullHandler()) if __name__ == '__main__': diff --git a/setup.py b/setup.py index 9ccbd42e..f9c3d9e9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ """ import setuptools -with open("README.rst") as fh: +with open("README.rst", encoding="utf-8") as fh: long_description = fh.read() setup_kwargs = { diff --git a/tests/conftest.py b/tests/conftest.py index 0af0f63b..8041c324 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,13 +9,11 @@ import pytest import salt.utils.yaml as yaml from pytestskipmarkers.utils import ports -from saltfactories.utils import random_string -from saltfactories.utils import running_username +from saltfactories.utils import random_string, running_username import pepper.script # Import pytest libraries -# Import Salt Libraries log = logging.getLogger(__name__) @@ -77,7 +75,9 @@ def session_ssh_roster_config(session_sshd_server, session_master): mine_functions: test.arg: ['itworked'] """.format( - session_sshd_server.listen_port, running_username(), session_sshd_server.client_key + session_sshd_server.listen_port, + running_username(), + session_sshd_server.client_key, ) with pytest.helpers.temp_file( "roster", roster_contents, session_master.config_dir @@ -124,7 +124,7 @@ def pepperconfig(salt_api_port): salt_api_port ) ) - with open("tests/.pepperrc", "w") as pepper_file: + with open("tests/.pepperrc", "w", encoding="utf-8") as pepper_file: print(config, file=pepper_file) yield os.remove("tests/.pepperrc") @@ -132,7 +132,7 @@ def pepperconfig(salt_api_port): @pytest.fixture def pepper_client(session_salt_api, salt_api_port): - client = pepper.Pepper("http://localhost:{}".format(salt_api_port)) + client = pepper.Pepper(f"http://localhost:{salt_api_port}") client.login("pepper", "pepper", "sharedsecret") return client @@ -155,16 +155,21 @@ def output_file(): @pytest.fixture(params=["/run", "/login"]) -def pepper_cli(request, session_salt_api, salt_api_port, output_file, session_sshd_server): +def pepper_cli( + request, session_salt_api, salt_api_port, output_file, session_sshd_server +): """ Wrapper to invoke Pepper with common params and inside an empty env """ - if request.config.getoption("--salt-api-backend") == "rest_tornado" and request.param == "/run": + if ( + request.config.getoption("--salt-api-backend") == "rest_tornado" + and request.param == "/run" + ): pytest.xfail("rest_tornado does not support /run endpoint until next release") def_args = [ "--out=json", - "--output-file={}".format(output_file), + f"--output-file={output_file}", "-c", "tests/.pepperrc", ] @@ -173,20 +178,22 @@ def pepper_cli(request, session_salt_api, salt_api_port, output_file, session_ss def_args = ["--run-uri"] + def_args def _run_pepper_cli(*args, **kwargs): - sys.argv = ["pepper", "-p", kwargs.pop("profile", "main")] + def_args + list(args) + sys.argv = ( + ["pepper", "-p", kwargs.pop("profile", "main")] + def_args + list(args) + ) exitcode = pepper.script.Pepper()() try: - with open(output_file) as result: + with open(output_file, encoding="utf-8") as result: try: return yaml.load(result) except yaml.parser.ParserError: result.seek(0) return [ - yaml.load("{0}}}".format(ret).strip('"')) + yaml.load(f"{ret}}}".strip('"')) for ret in result.read().split('}"\n') if ret ] - except Exception as exc: + except Exception as exc: # pylint: disable=broad-exception-caught log.error("ExitCode %s: %s", exitcode, exc) return exitcode @@ -256,7 +263,9 @@ def session_minion_factory(session_master_factory): """Return a factory for a randomly named minion connected to master.""" minion_factory = session_master_factory.salt_minion_daemon(random_string("minion-")) minion_factory.after_terminate( - pytest.helpers.remove_stale_minion_key, session_master_factory, minion_factory.id + pytest.helpers.remove_stale_minion_key, + session_master_factory, + minion_factory.id, ) return minion_factory @@ -301,6 +310,27 @@ def session_salt_api(session_master, session_salt_api_factory): yield session_salt_api_factory +@pytest.fixture(autouse=True) +def isolated_environment(monkeypatch, tmp_path): + """ + Remove pepper and salt-api settings inherited from the calling shell, and + point the home directory at an empty one. + + Pepper gives environment variables precedence over the config file, so a + developer with SALTAPI_URL exported would otherwise send the test + credentials to that salt-api instead of the one started for the tests. + With the real home directory, tests would also read the developer's + ~/.pepperrc and write their ~/.peppercache. + """ + for name in list(os.environ): + if name.startswith(("SALTAPI_", "PEPPER")) or name == "MASTER_CONFIG": + monkeypatch.delenv(name) + home = tmp_path / "home" + home.mkdir() + monkeypatch.setenv("HOME", str(home)) + monkeypatch.setenv("USERPROFILE", str(home)) + + def pytest_addoption(parser): parser.addoption( "--salt-api-backend", diff --git a/tests/integration/test_clients.py b/tests/integration/test_clients.py index 6a00be33..abb48e96 100644 --- a/tests/integration/test_clients.py +++ b/tests/integration/test_clients.py @@ -2,6 +2,7 @@ import pathlib import pytest +import salt.version def test_local_bad_opts(pepper_cli): @@ -28,7 +29,10 @@ def test_runner_client(pepper_cli): "two=what", "three={}".format(json.dumps({"hello": "world"})), ) - assert ret == {"args": ["one"], "kwargs": {"three": {"hello": "world"}, "two": "what"}} + assert ret == { + "args": ["one"], + "kwargs": {"three": {"hello": "world"}, "two": "what"}, + } @pytest.mark.xfail( @@ -54,7 +58,9 @@ def test_wheel_client_kwargs(pepper_cli, session_master): assert ret == "Wrote pepper.conf" default_include_dir = pathlib.Path(session_master.config["default_include"]).parent - pepper_config = pathlib.Path(session_master.config_dir) / default_include_dir / "pepper.conf" + pepper_config = ( + pathlib.Path(session_master.config_dir) / default_include_dir / "pepper.conf" + ) assert pepper_config.exists @@ -62,6 +68,10 @@ def test_wheel_client_kwargs(pepper_cli, session_master): 'config.getoption("--salt-api-backend") == "rest_tornado"', reason="sshClient unimplemented for now on tornado", ) +@pytest.mark.xfail( + salt.version.__saltstack_version__.info >= (3008,), + reason="salt-api rejects the eauth token for the ssh client on Salt 3008+", +) def test_ssh_client(pepper_cli, session_ssh_roster_config): ret = pepper_cli("--client=ssh", "*", "test.ping") assert ret["ssh"]["localhost"]["return"] is True diff --git a/tests/integration/test_json.py b/tests/integration/test_json.py index c3fa4ce1..2e75d684 100644 --- a/tests/integration/test_json.py +++ b/tests/integration/test_json.py @@ -17,7 +17,7 @@ def test_local_json_bad(pepper_cli): def test_local_json_file(pepper_cli, session_minion_id): tmpjson = os.path.join(tempfile.mkdtemp(), "json") - with open(tmpjson, "w") as tmpfile: + with open(tmpjson, "w", encoding="utf-8") as tmpfile: print( '[{"client": "local", "tgt": "*", "fun": "test.ping"}]', file=tmpfile, @@ -29,7 +29,7 @@ def test_local_json_file(pepper_cli, session_minion_id): def test_local_json_file_bad(pepper_cli): tmpjson = os.path.join(tempfile.mkdtemp(), "json") - with open(tmpjson, "w") as tmpfile: + with open(tmpjson, "w", encoding="utf-8") as tmpfile: print( "{what}", file=tmpfile, diff --git a/tests/integration/test_login_opts.py b/tests/integration/test_login_opts.py index d9c4b86b..784c1ee7 100644 --- a/tests/integration/test_login_opts.py +++ b/tests/integration/test_login_opts.py @@ -1,7 +1,7 @@ def test_cli_opts(pepper_cli, session_minion_id, salt_api_port): """Test the using a profile""" ret = pepper_cli( - "--saltapi-url=http://localhost:{}/".format(salt_api_port), + f"--saltapi-url=http://localhost:{salt_api_port}/", "--eauth=sharedsecret", "--username=pepper", "--password=pepper", @@ -28,7 +28,7 @@ def test_cli_opts_not_in_profile(pepper_cli, session_minion_id, salt_api_port): def test_cli_api_not_in_profile(pepper_cli, session_minion_id, salt_api_port): """Test the using a profile""" ret = pepper_cli( - "--saltapi-url=http://localhost:{}/".format(salt_api_port), + f"--saltapi-url=http://localhost:{salt_api_port}/", "*", "test.ping", profile="noapi", diff --git a/tests/integration/test_token.py b/tests/integration/test_token.py index 9b8131dd..d9d7616b 100644 --- a/tests/integration/test_token.py +++ b/tests/integration/test_token.py @@ -10,7 +10,9 @@ def test_local_token(tokfile, pepper_cli, session_minion_id): def test_runner_token(tokfile, pepper_cli): """Test runner execution with token file""" - ret = pepper_cli("-x", tokfile, "--make-token", "--client", "runner", "test.metasyntactic") + ret = pepper_cli( + "-x", tokfile, "--make-token", "--client", "runner", "test.metasyntactic" + ) exps = [ "foo", "bar", @@ -33,9 +35,11 @@ def test_runner_token(tokfile, pepper_cli): def test_token_expire(tokfile, pepper_cli): """Test token override param""" now = time.time() - pepper_cli("-x", tokfile, "--make-token", "--token-expire", "94670856", "*", "test.ping") + pepper_cli( + "-x", tokfile, "--make-token", "--token-expire", "94670856", "*", "test.ping" + ) - with open(tokfile) as tfile: + with open(tokfile, encoding="utf-8") as tfile: token = json.load(tfile) diff = (now + float(94670856)) - token["expire"] # Allow for 10-second window between request and master-side auth. diff --git a/tests/requirements.txt b/tests/requirements.txt index 083f792c..3430bc8d 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,13 +1,5 @@ -mock pytest>=6.0.0 -flake8-pyproject pytest-rerunfailures pytest-cov -pytest-salt-factories==1.0.4 +pytest-salt-factories==1.0.5 CherryPy -setuptools_scm==7.1.0 -importlib-metadata<5.0.0 -pyzmq>=25.1.2 ; python_version < '3.13' -pyzmq>=26.2.0 ; python_version >= '3.13' -cryptography>=42.0.0; python_version < '3.13' -cryptography==42.0.2; python_version >= '3.13' diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index dee39761..55dcb50e 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -1,24 +1,31 @@ -# Import Python Libraries -import imp +import importlib.util import os -import shutil - -import setuptools_scm +from importlib.metadata import PackageNotFoundError, version +from unittest.mock import patch import pepper -# Import Pepper Libraries - -def test_no_setup(): - setuppath = os.path.join(os.path.dirname(pepper.__file__), os.pardir, "setup.py") - shutil.move(setuppath, setuppath + ".bak") - ptest = imp.load_source("ptest", os.path.join(os.path.dirname(pepper.__file__), "__init__.py")) - shutil.move(setuppath + ".bak", setuppath) - assert ptest.version == setuptools_scm.get_version() - assert ptest.sha is None +def _load_pepper_init(name): + """ + Execute a fresh copy of pepper/__init__.py without touching the imported module + """ + path = os.path.join(os.path.dirname(pepper.__file__), "__init__.py") + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module def test_version(): - assert pepper.version == setuptools_scm.get_version() + assert pepper.__version__ == version("salt_pepper") + assert pepper.version == pepper.__version__ assert pepper.sha is None + + +def test_version_not_installed(): + with patch("importlib.metadata.version", side_effect=PackageNotFoundError): + ptest = _load_pepper_init("ptest") + assert ptest.__version__ is None + assert ptest.version is None + assert ptest.sha is None diff --git a/tests/unit/test_json_file.py b/tests/unit/test_json_file.py new file mode 100644 index 00000000..a4141eaf --- /dev/null +++ b/tests/unit/test_json_file.py @@ -0,0 +1,16 @@ +import sys +from unittest.mock import MagicMock + +import pytest + +import pepper.cli +from pepper.exceptions import PepperArgumentsException + + +def test_json_file_not_utf8(tmp_path): + path = tmp_path / "command.json" + path.write_bytes(b'[{"client": "local", "tgt": "\\xff\xff"}]') + sys.argv = ["pepper", "--json-file", str(path)] + cli = pepper.cli.PepperCli() + with pytest.raises(PepperArgumentsException, match="Invalid JSON given"): + cli.parse_cmd(MagicMock()) diff --git a/tests/unit/test_login_details.py b/tests/unit/test_login_details.py index 64e645ed..6108b504 100644 --- a/tests/unit/test_login_details.py +++ b/tests/unit/test_login_details.py @@ -1,11 +1,8 @@ import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper.cli -# Import Testing libraries - def test_interactive_logins(): sys.argv = ["pepper", "-c", "tests/.pepperrc", "-p", "noopts"] diff --git a/tests/unit/test_retcodes_error.py b/tests/unit/test_retcodes_error.py index f76d61fc..7ef8da04 100644 --- a/tests/unit/test_retcodes_error.py +++ b/tests/unit/test_retcodes_error.py @@ -1,13 +1,9 @@ -# Import Python Libraries import sys import pytest import pepper -# Import Pepper Libraries -# Import Testing Libraries - def test_fail_any(): sys.argv = ["pepper", "--fail-all", "--fail-any", "minion_id", "request"] diff --git a/tests/unit/test_retcodes_fail_fail.py b/tests/unit/test_retcodes_fail_fail.py index fd60b37c..05941262 100644 --- a/tests/unit/test_retcodes_fail_fail.py +++ b/tests/unit/test_retcodes_fail_fail.py @@ -1,12 +1,8 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { diff --git a/tests/unit/test_retcodes_fail_none.py b/tests/unit/test_retcodes_fail_none.py index 807640a3..0b3c4df7 100644 --- a/tests/unit/test_retcodes_fail_none.py +++ b/tests/unit/test_retcodes_fail_none.py @@ -1,12 +1,8 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { diff --git a/tests/unit/test_retcodes_fail_pass.py b/tests/unit/test_retcodes_fail_pass.py index 3334e329..3c707aff 100644 --- a/tests/unit/test_retcodes_fail_pass.py +++ b/tests/unit/test_retcodes_fail_pass.py @@ -1,12 +1,8 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { @@ -15,7 +11,11 @@ "ret": "/bin/sh: 123: command not found", "retcode": 127, }, - "saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0}, + "saltstack.ezh.msk.ru": { + "jid": "20180414193904158892", + "ret": "pass", + "retcode": 0, + }, } ] } diff --git a/tests/unit/test_retcodes_none_none.py b/tests/unit/test_retcodes_none_none.py index 99077aa2..3f34ab20 100644 --- a/tests/unit/test_retcodes_none_none.py +++ b/tests/unit/test_retcodes_none_none.py @@ -1,12 +1,8 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { diff --git a/tests/unit/test_retcodes_pass_fail.py b/tests/unit/test_retcodes_pass_fail.py index e148c0bc..6b284905 100644 --- a/tests/unit/test_retcodes_pass_fail.py +++ b/tests/unit/test_retcodes_pass_fail.py @@ -1,16 +1,16 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { - "saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0}, + "saltstack.ezh.msk.ru": { + "jid": "20180414193904158892", + "ret": "pass", + "retcode": 0, + }, "ezh.msk.ru": { "jid": "20180414193904158892", "ret": "/bin/sh: 123: command not found", diff --git a/tests/unit/test_retcodes_pass_none.py b/tests/unit/test_retcodes_pass_none.py index 41d14363..2f9fdc47 100644 --- a/tests/unit/test_retcodes_pass_none.py +++ b/tests/unit/test_retcodes_pass_none.py @@ -1,16 +1,16 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { - "saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0}, + "saltstack.ezh.msk.ru": { + "jid": "20180414193904158892", + "ret": "pass", + "retcode": 0, + }, "ezh.msk.ru": { "jid": "20180414193904158892", "ret": "Hello from SaltStack", diff --git a/tests/unit/test_retcodes_pass_pass.py b/tests/unit/test_retcodes_pass_pass.py index f84e5aa1..b71299eb 100644 --- a/tests/unit/test_retcodes_pass_pass.py +++ b/tests/unit/test_retcodes_pass_pass.py @@ -1,17 +1,17 @@ -# Import Python Libraries import sys -from unittest.mock import MagicMock -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pepper -# Import Pepper Libraries - PAYLOAD = { "return": [ { "ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0}, - "saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0}, + "saltstack.ezh.msk.ru": { + "jid": "20180414193904158892", + "ret": "pass", + "retcode": 0, + }, } ] } diff --git a/tests/unit/test_token.py b/tests/unit/test_token.py index 579d1de9..19c890e2 100644 --- a/tests/unit/test_token.py +++ b/tests/unit/test_token.py @@ -1,14 +1,9 @@ import json import sys -from unittest.mock import MagicMock -from unittest.mock import mock_open -from unittest.mock import patch +from unittest.mock import MagicMock, mock_open, patch import pepper.cli -# Import Pepper Libraries -# Import Testing Libraries - def test_token(): sys.argv = ["pepper", "*", "test.ping"]