diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..31726b0 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,16 @@ +# Changes here will be overwritten by Copier +_commit: ec5afd9 +_src_path: git@github.com:lhupfeldt/copier_python_package_template.git +author_email: lhn@hupfeldtit.dk +author_name: Lars Hupfeldt Nielsen +company_name: Hupfeldt IT +copyright_holder: Lars Hupfeldt Nielsen, Hupfeldt IT ApS +creation_year: 2012 +description: Python API with high level build flow constructs (parallel/serial) for + Jenkins. +git_repo_url: https://github.com/lhupfeldt/jenkinsflow.git +github_organization: lhupfeldt +legal_entity: ApS +local_devel_dependency: '' +module_name: jenkinsflow +package_name: jenkinsflow diff --git a/.gitignore b/.gitignore index 9799f64..7cb5683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,21 @@ -*.py[cod] - -# C extensions -*.so - -# Packages +*~ +*.pyc +__pycache__ +.cache +.coverage +.pytest_cache *.egg *.egg-info +TAGS +*.class .eggs -develop-eggs dist -build -parts -bin -var -sdist -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage* -coverage_rc.tenjin.cache* .tox .nox -nosetests.xml -.pytest_cache/ - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Emacs backup -*~ -TAGS -*_flymake.py - +.mypy_cache *.tenjin.cache -.cache -.cache-* -__pycache__ - -# Vim swap -*.sw? - -# PyChrm files -.idea +build +test/out # Sphinx doc/_build/* diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..90a1312 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,641 @@ +[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,.git,.svn + +# 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. +# LHN modified +load-plugins=pylint_pytest + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# 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. +# LHN modified +good-names=ii, + jj, + kk, + ex, + Run, + _, + fn, # file name + ff, # file name object + fh, # file handle + ln, # line + dt # date_time object + +# 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 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, + __post_init__ + +# 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] + +# 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. +# LHN modified +max-args=6 + +# Maximum number of attributes for a class (see R0902). +# LHN modified +max-attributes=12 + +# 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=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. +# LHN modified +max-returns=10 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +# LHN modified +min-public-methods=1 + + +[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= + +# 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. +# LHN modified +max-line-length=180 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# 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". +# LHN modified +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma + +# 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=c-extension-no-member + + +[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, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# 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. Available dictionaries: da_DK (hunspell), en_AG +# (hunspell), en_AU (hunspell), en_BS (hunspell), en_BW (hunspell), en_BZ +# (hunspell), en_CA (hunspell), en_DK (hunspell), en_GB (hunspell), en_GH +# (hunspell), en_HK (hunspell), en_IE (hunspell), en_IN (hunspell), en_JM +# (hunspell), en_MW (hunspell), en_NA (hunspell), en_NG (hunspell), en_NZ +# (hunspell), en_PH (hunspell), en_SG (hunspell), en_TT (hunspell), en_US +# (hunspell), en_ZA (hunspell), en_ZM (hunspell), en_ZW (hunspell). +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 diff --git a/LICENSE.TXT b/LICENSE.txt similarity index 95% rename from LICENSE.TXT rename to LICENSE.txt index 7fdf47f..8f1e3fd 100644 --- a/LICENSE.TXT +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2012 - 2015, Hupfeldt IT ApS +Copyright (c) 2012 - 2024 Lars Hupfeldt Nielsen, Hupfeldt IT ApS All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Makefile b/Makefile deleted file mode 100644 index 2ca893e..0000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -.PHONY: all -all: - tox diff --git a/README.rst b/README.rst index 0a2feb7..65e9e27 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ |Build Status| |Coverage| |Documentation Status| |PyPi Package| |License| jenkinsflow -=========== +----------- Python API with high level build flow constructs (parallel/serial) for Jenkins. Allows full scriptable control over the execution diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e2d2802..0000000 --- a/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT -# All rights reserved. This work is under a BSD license, see LICENSE.TXT. - -# Package -from . import py_version_check diff --git a/cli/set_build_description.py b/cli/set_build_description.py deleted file mode 100755 index 6eff391..0000000 --- a/cli/set_build_description.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT -# All rights reserved. This work is under a BSD license, see LICENSE.TXT. - -import click - -from jenkinsflow.utils import set_build_description as usbd - - -# Generate twice for backwards compatibility, first (hidden) is the old name -for name, hidden in (("set_build_description", True), ("set-build-description", False)): - @click.command(name=name, hidden=hidden) - @click.option('--description', help="The description to set on the build") - @click.option('--replace/--no-replace', default=False, help="Replace existing description, if any, instead of appending.") - @click.option('--separator', default='\n', help="A separator to insert between any existing description and the new 'description' if 'replace' is not specified.") - @click.option('--username', help="User Name for Jenkin authentication with secured Jenkins") - @click.option('--password', help="Password of Jenkins User") - @click.option('--build-url', help='Build URL', envvar='BUILD_URL') - @click.option('--job-name', help='Job Name', envvar='JOB_NAME') - @click.option('--build-number', help="Build Number", type=click.INT, envvar='BUILD_NUMBER') - @click.option( - '--direct-url', - default=None, - help="Jenkins URL - preferably non-proxied. If not specified, the value of JENKINS_URL or HUDSON_URL environment variables will be used.") - def set_build_description(description, replace, separator, username, password, build_url, job_name, build_number, direct_url): - """Utility to set/append build description on a job build. - - When called from a Jenkins job you can leave out the '--build-url', '--job-name' and '--build-number' arguments, the BUILD_URL env variable will be used. - """ - - # %(file)s --job-name --build-number --description [--direct-url ] [--replace | --separator ] [(--username --password )] - - usbd.set_build_description(description, replace, separator, username, password, build_url, job_name, build_number, direct_url) - - if hidden: - set_build_description_hidden = set_build_description diff --git a/demo/basic.py b/demo/basic.py index a072861..78de57e 100755 --- a/demo/basic.py +++ b/demo/basic.py @@ -7,7 +7,7 @@ from jenkinsflow.flow import serial -from jenkinsflow.demo import get_jenkins_api +import lib.get_jenkins_api def main(api, securitytoken): @@ -29,4 +29,4 @@ def main(api, securitytoken): if __name__ == '__main__': - main(*get_jenkins_api.get_jenkins_api()) + main(*lib.get_jenkins_api.get_jenkins_api()) diff --git a/demo/calculated_flow.py b/demo/calculated_flow.py index 3bdc717..4ae6fe7 100755 --- a/demo/calculated_flow.py +++ b/demo/calculated_flow.py @@ -12,7 +12,7 @@ from jenkinsflow.flow import serial from jenkinsflow.unbuffered import UnBuffered -from jenkinsflow.demo import get_jenkins_api +import lib.get_jenkins_api # Unbuffered output does not work well in Jenkins/Hudson, so in case @@ -70,4 +70,4 @@ def main(api, securitytoken): if __name__ == '__main__': - main(*get_jenkins_api.get_jenkins_api()) + main(*lib.get_jenkins_api.get_jenkins_api()) diff --git a/demo/errors.py b/demo/errors.py index ecd570b..c364b2b 100755 --- a/demo/errors.py +++ b/demo/errors.py @@ -5,7 +5,7 @@ from jenkinsflow.flow import serial -from jenkinsflow.demo import get_jenkins_api +import lib.get_jenkins_api def main(api, securitytoken): @@ -31,4 +31,4 @@ def main(api, securitytoken): if __name__ == '__main__': - main(*get_jenkins_api.get_jenkins_api()) + main(*lib.get_jenkins_api.get_jenkins_api()) diff --git a/demo/hide_password.py b/demo/hide_password.py index 85e56fd..58546b6 100755 --- a/demo/hide_password.py +++ b/demo/hide_password.py @@ -5,7 +5,7 @@ from jenkinsflow.flow import serial -from jenkinsflow.demo import get_jenkins_api +import lib.get_jenkins_api def main(api, securitytoken): @@ -18,4 +18,4 @@ def main(api, securitytoken): if __name__ == '__main__': - main(*get_jenkins_api.get_jenkins_api()) + main(*lib.get_jenkins_api.get_jenkins_api()) diff --git a/demo/jenkinsflow b/demo/jenkinsflow deleted file mode 120000 index f589cbd..0000000 --- a/demo/jenkinsflow +++ /dev/null @@ -1 +0,0 @@ -../../jenkinsflow \ No newline at end of file diff --git a/demo/lib/__init__.py b/demo/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/demo_security.py b/demo/lib/demo_security.py similarity index 100% rename from demo/demo_security.py rename to demo/lib/demo_security.py diff --git a/demo/get_jenkins_api.py b/demo/lib/get_jenkins_api.py similarity index 86% rename from demo/get_jenkins_api.py rename to demo/lib/get_jenkins_api.py index 60cce5d..b2d6da9 100644 --- a/demo/get_jenkins_api.py +++ b/demo/lib/get_jenkins_api.py @@ -2,7 +2,7 @@ from jenkinsflow.jenkins_api import Jenkins -from jenkinsflow.demo import demo_security as security +from . import demo_security as security def get_jenkins_api(): diff --git a/demo/prefix.py b/demo/prefix.py index 8b1d79b..298028d 100755 --- a/demo/prefix.py +++ b/demo/prefix.py @@ -5,7 +5,7 @@ from jenkinsflow.flow import serial -from jenkinsflow.demo import demo_security as security +import lib.demo_security as security def main(api): diff --git a/doc/source/conf.py b/doc/source/conf.py index 04873b8..1f485f0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -15,7 +15,7 @@ import sys import os from os.path import join as jp -import subprocess +from importlib import metadata # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -25,7 +25,6 @@ sys.path.insert(0, _top_dir) sys.path.insert(0, os.path.dirname(_top_dir)) -import setup as jf_setup # -- General configuration ------------------------------------------------ @@ -55,16 +54,17 @@ master_doc = 'index' # General information about the project. -project = jf_setup.PROJECT_NAME -copyright = jf_setup.COPYRIGHT -author = u'Lars Hupfeldt Nielsen' +package_info = metadata.metadata("jenkinsflow") +project = package_info["Name"] +author = package_info["Author"] +copyright = f"Copyright © 2012 - 2024 {author}, Hupfeldt IT" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = str(subprocess.check_output([sys.executable, jp(_top_dir, 'setup.py'), '--version'])) +version = package_info["Version"] # The full version, including alpha/beta/rc tags. release = version @@ -277,7 +277,7 @@ epub_title = u'jenkinsflow' epub_author = author epub_publisher = author -epub_copyright = jf_setup.COPYRIGHT +epub_copyright = copyright # The basename for the epub file. It defaults to the project name. #epub_basename = u'jenkinsflow' diff --git a/doc/source/jenkinsflow.rst b/doc/source/jenkinsflow.rst index 92123ee..125736d 100644 --- a/doc/source/jenkinsflow.rst +++ b/doc/source/jenkinsflow.rst @@ -1,10 +1,10 @@ `jenkinsflow` ===================== -.. program-output:: cli/jenkinsflow --help +.. program-output:: jenkinsflow --help :cwd: ../.. -.. program-output:: cli/jenkinsflow set-build-description --help +.. program-output:: jenkinsflow set-build-description --help :cwd: ../.. You can also use :doc:`jenkinsflow.utils.set_build_description` in your python script. diff --git a/noxfile.py b/noxfile.py index c610822..322d4bb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,31 +1,60 @@ +"""nox https://nox.thea.codes/en/stable/ configuration""" + +# Use nox >= 2023.4.22 + import os import sys import argparse import errno +import glob from pathlib import Path -from os.path import join as jp import subprocess import nox -sys.path.append('.') + +_HERE = Path(__file__).absolute().parent +_TEST_DIR = _HERE/"test" +_DEMO_DIR = _HERE/"demo" +_DOC_DIR = _HERE/"doc" + +sys.path.extend((str(_HERE), str(_DEMO_DIR))) from test.framework.cfg import ApiType, str_to_apis, dirs from test.framework.nox_utils import cov_options_env, parallel_options from test.framework.pytest_options import add_options -_HERE = Path(__file__).resolve().parent -_TEST_DIR = _HERE/"test" -_DEMO_DIR = _HERE/"demo" -_DOC_DIR = _HERE/"doc" # Locally we have nox handle the different versions, but in each travis run there is only a single python which can always be found as just 'python' _PY_VERSIONS = ["3.12", "3.11", "3.10", "3.9"] if not os.environ.get("TRAVIS_PYTHON_VERSION") else ["python"] _IS_CI = os.environ.get("CI", "false").lower() == "true" +nox.options.error_on_missing_interpreters = True + +# @nox.session(python=_PY_VERSIONS, reuse_venv=True) +# def typecheck(session): +# session.install("-e", ".", "mypy>=1.5.1") +# session.run("mypy", str(_HERE/"src")) + + +# TODO: pylint-pytest does not support 3.12 +@nox.session(python="3.11", reuse_venv=True) +def pylint(session): + session.install(".", "pylint>=3.3.1", "pylint-pytest>=1.1.8") + + print("\nPylint src") + session.run("pylint", "--fail-under", "8.1", str(_HERE/"src")) + + print("\nPylint test sources") + disable_checks = "missing-module-docstring,missing-class-docstring,missing-function-docstring" + disable_checks += ",multiple-imports,invalid-name,duplicate-code" + session.run( + "pylint", "--fail-under", "9.1", "--variable-rgx", r"[a-z_][a-z0-9_]{1,30}$", "--disable", disable_checks, + "--ignore", "jenkins_security.py,demos_test.py", str(_TEST_DIR)) + @nox.session(python=_PY_VERSIONS, reuse_venv=True) -def test(session): +def unit(session): """ Test jenkinsflow. Runs all tests mocked in hyperspeed, runs against Jenkins, using jenkins_api, and run script_api jobs. @@ -56,8 +85,7 @@ def test(session): parallel = parsed_args.job_load or parsed_args.job_delete pytest_args.extend(parallel_options(parallel, apis)) - pytest_args.extend(["--capture=sys", "--instafail"]) - + pytest_args.extend(["--capture=sys", "--instafail", "-p", "no:warnings" "--failed-first"]) pytest_args.extend(session.posargs) try: @@ -74,8 +102,9 @@ def test(session): python_executable = f"{dirs.pseudo_install_dir}/bin/python" session.run(python_executable, "-m", "pip", "install", "--upgrade", ".") env["JEKINSFLOW_TEST_JENKINS_API_PYTHON_EXECUTABLE"] = python_executable - subprocess.check_call([_HERE/"test/tmp_install.sh", _TEST_DIR, jp(dirs.test_tmp_dir, "test")]) - subprocess.check_call([_HERE/"test/tmp_install.sh", _DEMO_DIR, jp(dirs.test_tmp_dir, "demo")]) + tmp_inst_script = _HERE/"test/framework/tmp_install.sh" + subprocess.check_call([tmp_inst_script, _TEST_DIR, f"{dirs.test_tmp_dir}/test"]) + subprocess.check_call([tmp_inst_script, _DEMO_DIR, f"{dirs.test_tmp_dir}/demo"]) except: print(f"Failed venv test installation to '{dirs.pseudo_install_dir}'", file=sys.stderr) raise @@ -86,8 +115,17 @@ def test(session): cov_opts, cov_env = cov_options_env(apis, True) env.update(cov_env) - # env["COVERAGE_DEBUG"] = "config" - session.run("pytest", "--capture=sys", *cov_opts, *pytest_args, env=env) + # env["COVERAGE_DEBUG"] = "config,trace,pathmap" + session.run("pytest", "--capture=sys", '--cov', *cov_opts, *pytest_args, env=env) + + +@nox.session(python=_PY_VERSIONS[0], reuse_venv=True) +def build(session): + session.install("build>=1.0.3", "twine>=4.0.2") + for ff in glob.glob("dist/*"): + os.remove(ff) + session.run("python", "-m", "build") + session.run("python", "-m", "twine", "check", "dist/*") @nox.session(python=_PY_VERSIONS[0], reuse_venv=True) diff --git a/py_version_check.py b/py_version_check.py deleted file mode 100644 index bcaf0d4..0000000 --- a/py_version_check.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys -import os.path - - -error_msg = """ -`{pkg}` 4.0+ supports Python {min_sup_version} and above. -When using Python 2.7, 3.4 - 3.5.x please install {pkg} 3.x - -Python {py} detected. - -Make sure you have pip >= 9.0 as well as setuptools >= 24.2 to avoid these kinds of issues: - - $ pip install pip setuptools --upgrade - -Your choices: - -- Upgrade to Python {min_sup_version}+. - -- Install an older version of {pkg}: - - $ pip install '{pkg}<9.0' - -It would be great if you can figure out how this version ended up being -installed, and try to check how to prevent that for future users. - -Source: https://github.com/lhupfeldt/{pkg} -""" - -min_sup_version = (3, 6, 0) -if sys.version_info < min_sup_version: - raise ImportError(error_msg.format( - py='.'.join([str(vv) for vv in sys.version_info[:3]]), - pkg=os.path.basename(os.path.dirname(os.path.abspath(__file__))), - min_sup_version='.'.join([str(vv) for vv in min_sup_version]))) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0896146 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +# Copyright © 2012 - 2024 Lars Hupfeldt Nielsen, Hupfeldt IT ApS + +[build-system] +requires = ["setuptools >= 68.0.0", "wheel>=0.40.0", "setuptools_scm[toml]>=7.1.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini index bb60f36..8a8cbf9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,6 @@ # Pytest config [pytest] -minversion = 2.9.2 +minversion = 7.4.1 testpaths = test -norecursedirs = __pycache__ utils cli doc dist demo -addopts = -p no:warnings --ff +norecursedirs = __pycache__ utils perf doc dist demo diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d550aea..0000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -setuptools>=69.0.3 -requests>=2.20,<=3.0 -atomicfile>=1.0,<=2.0 -click>=7.0 -tenjin>=1.1.1 - -# Required by the script API: -# You need to install python(3)-devel to be be able to install psutil, see INSTALL.md -psutil>=5.6.6 -setproctitle>=1.1.10 - -# Required by the job dependency graph visualisation -bottle>=0.12.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d21b887 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,65 @@ +[metadata] +name = jenkinsflow +url = https://github.com/lhupfeldt/jenkinsflow.git + +author = Lars Hupfeldt Nielsen +author_email = lhn@hupfeldtit.dk + +description = Python API with high level build flow constructs (parallel/serial) for Jenkins. +long_description = file: README.rst +long_description_content_type = text/x-rst + +license = BSD + +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Topic :: Software Development :: Libraries + +[options] +zip_safe = True +include_package_data = True +install_requires = + # Add your dependencies here + requests>=2.20,<=3.0 + atomicfile>=1.0,<=2.0 + click>=7.0 + tenjin>=1.1.1 + + # Required by the script API: + # You need to install python(3)-devel to be be able to install psutil, see INSTALL.md + psutil>=5.6.6 + setproctitle>=1.1.10 + + # Required by the job dependency graph visualisation + bottle>=0.12.1 + +python_requires = >= 3.9 + +packages = + jenkinsflow + jenkinsflow.utils + jenkinsflow.cli + +package_dir = + jenkinsflow = src + jenkinsflow.utils = src/utils + jenkinsflow.cli = src/cli + +[options.entry_points] +console_scripts = + jenkinsflow = jenkinsflow.cli.cli:main + +[options.extras_require] +dev = + nox + +[aliases] +test = nox diff --git a/setup.py b/setup.py index 29c4d8a..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,79 +1,3 @@ -import os - from setuptools import setup - -PROJECT_ROOT, _ = os.path.split(__file__) -PROJECT_NAME = 'jenkinsflow' -COPYRIGHT = u"Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT" -PROJECT_AUTHORS = u"Lars Hupfeldt Nielsen" -PROJECT_EMAILS = 'lhn@hupfeldtit.dk' -PROJECT_URL = "https://github.com/lhupfeldt/jenkinsflow" -SHORT_DESCRIPTION = 'Python API with high level build flow constructs (parallel/serial) for Jenkins (and Hudson).' -LONG_DESCRIPTION = open(os.path.join(PROJECT_ROOT, "README.rst")).read() - -on_rtd = os.environ.get('READTHEDOCS') == 'True' -is_ci = os.environ.get('CI', 'false').lower() == 'true' - -_here = os.path.dirname(os.path.abspath(__file__)) -with open(os.path.join(_here, 'py_version_check.py')) as ff: - exec(ff.read()) - -with open(os.path.join(_here, 'requirements.txt')) as ff: - install_requires=[req.strip() for req in ff.readlines() if req.strip() and req.strip()[0] != "#"] - -if __name__ == "__main__": - setup( - name=PROJECT_NAME.lower(), - version_command=('git describe', 'pep440-git'), - author=PROJECT_AUTHORS, - author_email=PROJECT_EMAILS, - packages=[ - 'jenkinsflow', - 'jenkinsflow.utils', - 'jenkinsflow.cli', - 'jenkinsflow.demo', - 'jenkinsflow.demo.jobs', - 'jenkinsflow.test', - 'jenkinsflow.test.framework', - 'jenkinsflow.test.framework.cfg', - ], - package_dir={ - 'jenkinsflow': '.', - 'jenkinsflow.utils': 'utils', - 'jenkinsflow.cli': 'cli', - 'jenkinsflow.demo': 'demo', - 'jenkinsflow.demo.jobs': 'demo/jobs', - 'jenkinsflow.test': 'test', - 'jenkinsflow.test.framework': 'test/framework', - 'jenkinsflow.test.framework.cfg': 'test/framework/cfg', - }, - zip_safe=True, - include_package_data=False, - python_requires='>=3.9.0', - install_requires=install_requires, - setup_requires='setuptools-version-command>=2.2', - test_suite='test', - url=PROJECT_URL, - description=SHORT_DESCRIPTION, - long_description=LONG_DESCRIPTION, - long_description_content_type='text/x-rst', - license='BSD', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.9', - 'Topic :: Software Development :: Testing', - ], - entry_points=''' - [console_scripts] - jenkinsflow=jenkinsflow.cli.cli:cli - ''', - ) +setup() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..2fc27cb --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,6 @@ +"""Set __version__ property.""" + +from importlib.metadata import version # type: ignore + + +__version__ = version("jenkinsflow") diff --git a/api_base.py b/src/api_base.py similarity index 100% rename from api_base.py rename to src/api_base.py diff --git a/checking_state.py b/src/checking_state.py similarity index 100% rename from checking_state.py rename to src/checking_state.py diff --git a/cli/__init__.py b/src/cli/__init__.py similarity index 100% rename from cli/__init__.py rename to src/cli/__init__.py diff --git a/cli/cli.py b/src/cli/cli.py similarity index 80% rename from cli/cli.py rename to src/cli/cli.py index 1f6f3b0..8e9a7da 100755 --- a/cli/cli.py +++ b/src/cli/cli.py @@ -15,7 +15,7 @@ __package__ = "jenkinsflow.cli" -from .set_build_description import set_build_description, set_build_description_hidden +from .set_build_description import set_build_description @click.group() @@ -24,8 +24,11 @@ def cli(): cli.add_command(set_build_description) -cli.add_command(set_build_description_hidden) # Backwards compatibility -if __name__ == "__main__": +def main(): cli(auto_envvar_prefix='JENKINSFLOW') + + +if __name__ == "__main__": + main() diff --git a/cli/jenkinsflow b/src/cli/jenkinsflow similarity index 100% rename from cli/jenkinsflow rename to src/cli/jenkinsflow diff --git a/src/cli/set_build_description.py b/src/cli/set_build_description.py new file mode 100755 index 0000000..4000f24 --- /dev/null +++ b/src/cli/set_build_description.py @@ -0,0 +1,30 @@ +# Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT +# All rights reserved. This work is under a BSD license, see LICENSE.TXT. + +import click + +from jenkinsflow.utils import set_build_description as usbd + + +@click.command() +@click.option('--description', help="The description to set on the build") +@click.option('--replace/--no-replace', default=False, help="Replace existing description, if any, instead of appending.") +@click.option('--separator', default='\n', help="A separator to insert between any existing description and the new 'description' if 'replace' is not specified.") +@click.option('--username', help="User Name for Jenkin authentication with secured Jenkins") +@click.option('--password', help="Password of Jenkins User") +@click.option('--build-url', help='Build URL', envvar='BUILD_URL') +@click.option('--job-name', help='Job Name', envvar='JOB_NAME') +@click.option('--build-number', help="Build Number", type=click.INT, envvar='BUILD_NUMBER') +@click.option( + '--direct-url', + default=None, + help="Jenkins URL - preferably non-proxied. If not specified, the value of JENKINS_URL or HUDSON_URL environment variables will be used.") +def set_build_description(description, replace, separator, username, password, build_url, job_name, build_number, direct_url): + """Utility to set/append build description on a job build. + + When called from a Jenkins job you can leave out the '--build-url', '--job-name' and '--build-number' arguments, the BUILD_URL env variable will be used. + """ + + # %(file)s --job-name --build-number --description [--direct-url ] [--replace | --separator ] [(--username --password )] + + usbd.set_build_description(description, replace, separator, username, password, build_url, job_name, build_number, direct_url) diff --git a/flow.py b/src/flow.py similarity index 100% rename from flow.py rename to src/flow.py diff --git a/flow_exceptions.py b/src/flow_exceptions.py similarity index 100% rename from flow_exceptions.py rename to src/flow_exceptions.py diff --git a/jenkins_api.py b/src/jenkins_api.py similarity index 100% rename from jenkins_api.py rename to src/jenkins_api.py diff --git a/jobload.py b/src/jobload.py similarity index 100% rename from jobload.py rename to src/jobload.py diff --git a/kill_type.py b/src/kill_type.py similarity index 100% rename from kill_type.py rename to src/kill_type.py diff --git a/ordered_enum.py b/src/ordered_enum.py similarity index 100% rename from ordered_enum.py rename to src/ordered_enum.py diff --git a/propagation_types.py b/src/propagation_types.py similarity index 100% rename from propagation_types.py rename to src/propagation_types.py diff --git a/src/py.typed b/src/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/rest_api_wrapper.py b/src/rest_api_wrapper.py similarity index 100% rename from rest_api_wrapper.py rename to src/rest_api_wrapper.py diff --git a/script_api.py b/src/script_api.py similarity index 100% rename from script_api.py rename to src/script_api.py diff --git a/speed.py b/src/speed.py similarity index 100% rename from speed.py rename to src/speed.py diff --git a/unbuffered.py b/src/unbuffered.py similarity index 100% rename from unbuffered.py rename to src/unbuffered.py diff --git a/utils/__init__.py b/src/utils/__init__.py similarity index 100% rename from utils/__init__.py rename to src/utils/__init__.py diff --git a/utils/set_build_description.py b/src/utils/set_build_description.py similarity index 100% rename from utils/set_build_description.py rename to src/utils/set_build_description.py diff --git a/utils/utils.py b/src/utils/utils.py similarity index 100% rename from utils/utils.py rename to src/utils/utils.py diff --git a/visual/README.md b/src/visual/README.md similarity index 100% rename from visual/README.md rename to src/visual/README.md diff --git a/visual/flow_vis.html b/src/visual/flow_vis.html similarity index 100% rename from visual/flow_vis.html rename to src/visual/flow_vis.html diff --git a/visual/js/graph.js b/src/visual/js/graph.js similarity index 100% rename from visual/js/graph.js rename to src/visual/js/graph.js diff --git a/visual/server.py b/src/visual/server.py similarity index 100% rename from visual/server.py rename to src/visual/server.py diff --git a/visual/stylesheets/flow.css b/src/visual/stylesheets/flow.css similarity index 100% rename from visual/stylesheets/flow.css rename to src/visual/stylesheets/flow.css diff --git a/visual/test/flow_graph.json b/src/visual/test/flow_graph.json similarity index 100% rename from visual/test/flow_graph.json rename to src/visual/test/flow_graph.json diff --git a/test/framework/coverage_rc b/test/.coveragerc similarity index 50% rename from test/framework/coverage_rc rename to test/.coveragerc index 7f24433..ade5185 100644 --- a/test/framework/coverage_rc +++ b/test/.coveragerc @@ -1,7 +1,16 @@ +[run] +branch = True +source = jenkinsflow + +[paths] +source = + src + **/site-packages/jenkinsflow + [report] # This will be overridden with command line option depending on chosen API type tests fail_under = 100 - +precision = 3 exclude_lines = # Have to re-enable the standard pragma pragma: no cover @@ -13,17 +22,23 @@ exclude_lines = # The alternative (original) rest api is no longer tested def _check_restkit_response class RestkitRestApi + + # Don't complain if tests don't hit defensive assertion code: + raise .*Internal error.* + raise .*AbstractNotImplemented.* + raise *\# Should not happen + ${COV_API_EXCLUDE_LINES?} +partial_branches = + # Have to re-enable the standard pragma + pragma: no branch + +omit = + .nox/* + test/* + experiments + *_flymake.py + visual/server.py -omit = - noxfile.py - .nox/* - .eggs/* - test/* - demo/* - visual/server.py - setup.py - ordered_enum.py - *_flymake.py - ${COV_API_EXCLUDE_FILES?} + ${COV_API_EXCLUDE_FILES?} diff --git a/test/abort_retry_test.py b/test/abort_retry_test.py index ca64af0..f29c690 100644 --- a/test/abort_retry_test.py +++ b/test/abort_retry_test.py @@ -16,14 +16,14 @@ @pytest.mark.not_apis(ApiType.SCRIPT) -def test_abort_retry_serial_toplevel(api_type): +def test_abort_retry_serial_toplevel(api_type, options): with api_select.api(__file__, api_type) as api: api.flow_job() api.job('j11', max_fails=0, expect_invocations=1, expect_order=1) api.job('j12_abort', max_fails=0, expect_invocations=1, expect_order=2, exec_time=20, serial=True, final_result='ABORTED') api.job('j13', max_fails=0, expect_invocations=0, expect_order=None, serial=True) - abort(api, 'j12_abort', 10) + abort(api, 'j12_abort', 10, options) with raises(FailedChildJobException): with serial(api, timeout=70, job_name_prefix=api.job_name_prefix, max_tries=2) as ctrl1: @@ -33,14 +33,14 @@ def test_abort_retry_serial_toplevel(api_type): @pytest.mark.not_apis(ApiType.SCRIPT) -def test_abort_retry_parallel_toplevel(api_type): +def test_abort_retry_parallel_toplevel(api_type, options): with api_select.api(__file__, api_type) as api: api.flow_job() api.job('j11', max_fails=0, expect_invocations=1, expect_order=None) api.job('j12_abort', max_fails=0, expect_invocations=1, expect_order=None, exec_time=20, final_result='ABORTED') api.job('j13', max_fails=0, expect_invocations=1, expect_order=None) - abort(api, 'j12_abort', 10) + abort(api, 'j12_abort', 10, options) with raises(FailedChildJobsException): with parallel(api, timeout=70, job_name_prefix=api.job_name_prefix, max_tries=2) as ctrl1: @@ -50,7 +50,7 @@ def test_abort_retry_parallel_toplevel(api_type): @pytest.mark.not_apis(ApiType.SCRIPT) -def test_abort_retry_serial_parallel_nested(api_type): +def test_abort_retry_serial_parallel_nested(api_type, options): with api_select.api(__file__, api_type) as api: api.flow_job() api.job('j11', max_fails=0, expect_invocations=1, expect_order=1) @@ -60,7 +60,7 @@ def test_abort_retry_serial_parallel_nested(api_type): api.job('j24', max_fails=0, expect_invocations=1, expect_order=2) api.job('j12', max_fails=0, expect_invocations=0, expect_order=None, serial=True) - abort(api, 'j22_abort', 10) + abort(api, 'j22_abort', 10, options) with raises(FailedChildJobException): with serial(api, timeout=70, job_name_prefix=api.job_name_prefix, max_tries=2) as sctrl1: @@ -74,7 +74,7 @@ def test_abort_retry_serial_parallel_nested(api_type): @pytest.mark.not_apis(ApiType.SCRIPT) -def test_abort_retry_parallel_serial_nested(api_type): +def test_abort_retry_parallel_serial_nested(api_type, options): with api_select.api(__file__, api_type) as api: api.flow_job() api.job('j11', max_fails=0, expect_invocations=1, expect_order=None) @@ -84,7 +84,7 @@ def test_abort_retry_parallel_serial_nested(api_type): api.job('j24', max_fails=0, expect_invocations=0, expect_order=None) api.job('j12', max_fails=0, expect_invocations=1, expect_order=1, serial=True) - abort(api, 'j22_abort', 10) + abort(api, 'j22_abort', 10, options) with raises(FailedChildJobsException): with parallel(api, timeout=70, job_name_prefix=api.job_name_prefix, max_tries=2) as sctrl1: diff --git a/test/abort_test.py b/test/abort_test.py index 61b918e..c19a74d 100644 --- a/test/abort_test.py +++ b/test/abort_test.py @@ -17,14 +17,14 @@ @pytest.mark.not_apis(ApiType.SCRIPT) -def test_abort(api_type, capsys): +def test_abort(api_type, capsys, options): with api_select.api(__file__, api_type, login=True) as api: api.flow_job() api.job('quick', max_fails=0, expect_invocations=1, expect_order=1) api.job('wait10_abort', max_fails=0, expect_invocations=1, expect_order=1, exec_time=20, final_result='ABORTED') api.job('wait1_fail', max_fails=1, expect_invocations=1, expect_order=1, exec_time=1) - abort(api, 'wait10_abort', 10) + abort(api, 'wait10_abort', 8, options) with raises(FailedChildJobsException) as exinfo: with parallel(api, timeout=40, job_name_prefix=api.job_name_prefix, report_interval=3) as ctrl: diff --git a/test/auth_error_test.py b/test/auth_error_test.py index fe2b18c..fbf51c8 100644 --- a/test/auth_error_test.py +++ b/test/auth_error_test.py @@ -23,4 +23,3 @@ def test_auth_error(api_type): assert ("401 Client Error: Unauthorized for url: http://" in str(exinfo.value) or "401 Client Error: Invalid password/token for user: noaccess for url: http://" in str(exinfo.value) or "401 Unauthorized user: 'noaccess' for url: http://" in str(exinfo.value)) - diff --git a/test/conftest.py b/test/conftest.py index f03e195..463c102 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,24 +1,57 @@ -# Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT -# All rights reserved. This work is under a BSD license, see LICENSE.TXT. +"""Configuration file for 'pytest'""" -import os import sys +import os import re from itertools import chain -from typing import List +from pathlib import Path +import shutil import pytest -from pytest import fixture # pylint: disable=no-name-in-module +from pytest import fixture from click.testing import CliRunner from .framework import pytest_options -from .framework.cfg import ApiType, opts_to_test_cfg +from .framework.cfg import ApiType, AllCfg, opts_to_test_cfg # Note: You can't (indirectly) import stuff from jenkinsflow here, it messes up the coverage +_HERE = Path(__file__).absolute().parent +_DEMO_DIR = (_HERE/"../demo").resolve() +sys.path.append(str(_DEMO_DIR)) + +_OUT_DIRS = {} + +def _test_key_shortener(key_prefix, key_suffix): + prefix = key_prefix.replace('test.', '').replace('_test', '') + suffix = key_suffix.replace(prefix, '').replace('test_', '').strip('_') + outd = prefix + '.' + suffix + args = (key_prefix, key_suffix) + assert _OUT_DIRS.setdefault(outd, args) == args, \ + f"Out dir name '{outd}' reused! Previous from {_OUT_DIRS[outd]}, now {args}. Test is not following namimg convention." + return outd + + +def _test_node_shortener(request): + """Shorten test node name while still keeping it unique""" + return _test_key_shortener(request.node.module.__name__, request.node.name.split('[')[0]) + -# Singleton config -TEST_CFG = None +@fixture(name="out_dir") +def _fixture_out_dir(request): + """Create unique top level test directory for a test.""" + + out_dir = _HERE/'out'/_test_node_shortener(request) + + try: + shutil.rmtree(out_dir) + except OSError as ex: + if ex.errno != errno.ENOENT: + raise + + return out_dir + +# Add you configuration, e.g. fixtures here. def pytest_addoption(parser): @@ -27,24 +60,26 @@ def pytest_addoption(parser): def pytest_configure(config): - global TEST_CFG - """pytest hook""" # Register api marker config.addinivalue_line("markers", "apis(*ApiType): mark test to run only when using specified apis") config.addinivalue_line("markers", "not_apis(*ApiType): mark test NOT to run when using specified apis") - TEST_CFG = opts_to_test_cfg( + config.cuctom_cfg = opts_to_test_cfg( config.getoption(pytest_options.OPT_DIRECT_URL), config.getoption(pytest_options.OPT_JOB_LOAD), config.getoption(pytest_options.OPT_JOB_DELETE), config.getoption(pytest_options.OPT_MOCK_SPEEDUP), config.getoption(pytest_options.OPT_API), ) - config.cuctom_cfg = TEST_CFG + pytest._CUSTOM_TEST_CFG = config.cuctom_cfg + + +def get_cfg(): + return pytest._CUSTOM_TEST_CFG -def pytest_collection_modifyitems(items: List[pytest.Item], config) -> None: +def pytest_collection_modifyitems(items: list[pytest.Item], config) -> None: """pytest hook""" selected_api_types = config.cuctom_cfg.apis item_api_type_regex = re.compile(r'.*\[ApiType\.(.*)\]') @@ -79,10 +114,10 @@ def filter_items_by_api_type(item): items[:] = remaining -@pytest.fixture() -def options(): +@pytest.fixture(scope="session") +def options(pytestconfig): """Access to test configuration objects.""" - return TEST_CFG + return pytestconfig.cuctom_cfg @pytest.fixture(params=list(ApiType)) diff --git a/test/demos/__init__.py b/test/demos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/demos/jobs/__init__.py b/test/demos/jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/jobs/basic_jobs.py b/test/demos/jobs/basic_jobs.py similarity index 93% rename from demo/jobs/basic_jobs.py rename to test/demos/jobs/basic_jobs.py index e840f08..14de4bb 100644 --- a/demo/jobs/basic_jobs.py +++ b/test/demos/jobs/basic_jobs.py @@ -1,7 +1,7 @@ # Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT # All rights reserved. This work is under a BSD license, see LICENSE.TXT. -from jenkinsflow.test.framework import api_select +from ...framework import api_select def create_jobs(api_type): diff --git a/demo/jobs/calculated_flow_jobs.py b/test/demos/jobs/calculated_flow_jobs.py similarity index 96% rename from demo/jobs/calculated_flow_jobs.py rename to test/demos/jobs/calculated_flow_jobs.py index bcbf51d..a9a40f8 100644 --- a/demo/jobs/calculated_flow_jobs.py +++ b/test/demos/jobs/calculated_flow_jobs.py @@ -3,7 +3,7 @@ from collections import OrderedDict -from jenkinsflow.test.framework import api_select +from ...framework import api_select def create_jobs(api_type): diff --git a/demo/jobs/errors_jobs.py b/test/demos/jobs/errors_jobs.py similarity index 95% rename from demo/jobs/errors_jobs.py rename to test/demos/jobs/errors_jobs.py index d14aaa8..a8e4915 100644 --- a/demo/jobs/errors_jobs.py +++ b/test/demos/jobs/errors_jobs.py @@ -1,7 +1,7 @@ # Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT # All rights reserved. This work is under a BSD license, see LICENSE.TXT. -from jenkinsflow.test.framework import api_select +from ...framework import api_select def create_jobs(api_type): diff --git a/demo/jobs/hide_password_jobs.py b/test/demos/jobs/hide_password_jobs.py similarity index 91% rename from demo/jobs/hide_password_jobs.py rename to test/demos/jobs/hide_password_jobs.py index cd854ee..6e01a04 100644 --- a/demo/jobs/hide_password_jobs.py +++ b/test/demos/jobs/hide_password_jobs.py @@ -1,7 +1,7 @@ # Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT # All rights reserved. This work is under a BSD license, see LICENSE.TXT. -from jenkinsflow.test.framework import api_select +from ...framework import api_select def create_jobs(api_type): diff --git a/demo/jobs/prefix_jobs.py b/test/demos/jobs/prefix_jobs.py similarity index 92% rename from demo/jobs/prefix_jobs.py rename to test/demos/jobs/prefix_jobs.py index 38ad18e..18ec944 100644 --- a/demo/jobs/prefix_jobs.py +++ b/test/demos/jobs/prefix_jobs.py @@ -1,7 +1,7 @@ # Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT # All rights reserved. This work is under a BSD license, see LICENSE.TXT. -from jenkinsflow.test.framework import api_select +from ...framework import api_select def create_jobs(api_type): diff --git a/test/demos_test.py b/test/demos_test.py index 88f2f66..f7ee30c 100644 --- a/test/demos_test.py +++ b/test/demos_test.py @@ -1,8 +1,9 @@ # Copyright (c) 2012 - 2015 Lars Hupfeldt Nielsen, Hupfeldt IT # All rights reserved. This work is under a BSD license, see LICENSE.TXT. -import importlib.util -import importlib.machinery +import sys +import importlib +# import importlib.machinery from pathlib import Path import pytest @@ -10,39 +11,27 @@ from jenkinsflow.flow import parallel, JobControlFailException -from jenkinsflow.demo import basic, calculated_flow, prefix, hide_password, errors +_HERE = Path(__file__).absolute().parent +_DEMO_JOBS_DIR = _HERE/"demos/jobs" + +from demo import basic, calculated_flow, prefix, hide_password, errors from .framework import api_select from .framework.cfg import ApiType -_HERE = Path(__file__).resolve().parent -_DEMO_JOBS_DIR = (_HERE/"../demo/jobs").resolve() - - -def _load_source(modname, filename): - # https://docs.python.org/3/whatsnew/3.12.html#imp - loader = importlib.machinery.SourceFileLoader(modname, filename) - spec = importlib.util.spec_from_file_location(modname, filename, loader=loader) - module = importlib.util.module_from_spec(spec) - # The module is always executed and not cached in sys.modules. - # Uncomment the following line to cache the module. - # sys.modules[module.__name__] = module - loader.exec_module(module) - return module - - def load_demo_jobs(demo, api_type): print("\nLoad jobs for demo:", demo.__name__) - simple_demo_name = demo.__name__.replace("jenkinsflow.", "").replace("demo.", "") - job_load_module_name = simple_demo_name + '_jobs' - job_load = _load_source(job_load_module_name, str(_DEMO_JOBS_DIR/(job_load_module_name + '.py'))) + simple_demo_name = demo.__name__.replace("demo.", "") + job_load_module_name = ".demos.jobs." + simple_demo_name + '_jobs' + job_load = importlib.import_module(job_load_module_name, "test") api = job_load.create_jobs(api_type) flow_job_name = simple_demo_name + "__0flow" return flow_job_name def _test_demo(demo, api_type): + print("_test_demo", demo, api_type) flow_job_name = load_demo_jobs(demo, api_type) with api_select.api(__file__, api_type, fixed_prefix="jenkinsflow_demo__") as api: diff --git a/test/framework/abort_job.py b/test/framework/abort_job.py index d4826b0..e3dbcc4 100644 --- a/test/framework/abort_job.py +++ b/test/framework/abort_job.py @@ -6,7 +6,6 @@ import subprocess from pathlib import Path -from jenkinsflow.test.conftest import TEST_CFG from . import api_select from .logger import log, logt from .cfg import ApiType, AllCfg, opt_strs_to_test_cfg, test_cfg_to_opt_strs @@ -46,15 +45,15 @@ def _abort(log_file, test_file_name, api_type, fixed_prefix, job_name, sleep_tim raise -def abort(api, job_name, sleep_time): +def abort(api, job_name, sleep_time, test_cfg: AllCfg): """Call this script as a subprocess""" if api.api_type == ApiType.MOCK: return - args = [sys.executable, "-m", "jenkinsflow.test.framework.abort_job", + args = [sys.executable, "-m", f"jenkinsflow.test.framework.{Path(__file__).stem}", api.file_name, api.api_type.name, api.func_name.replace('test_', ''), job_name, str(sleep_time), - *test_cfg_to_opt_strs(TEST_CFG, api.api_type) - ] + *test_cfg_to_opt_strs(test_cfg, api.api_type)] with open(job_name + '.log', 'w') as log_file: + logt(log_file, "Current dir:", Path.cwd()) logt(log_file, "Invoking abort subprocess.", args) - subprocess.Popen(args) + subprocess.Popen(args, start_new_session=True) diff --git a/test/framework/api_select.py b/test/framework/api_select.py index a34c220..88050d2 100644 --- a/test/framework/api_select.py +++ b/test/framework/api_select.py @@ -5,7 +5,7 @@ from jenkinsflow.unbuffered import UnBuffered -from jenkinsflow.test.conftest import TEST_CFG +from ..conftest import get_cfg from .cfg import ApiType, AllCfg from .cfg.speedup import speedup @@ -19,7 +19,7 @@ def api(file_name, api_type, login=None, fixed_prefix=None, url_or_dir=None, fake_public_uri=None, invocation_class=None, username=None, password=None, *, options: AllCfg = None): """Factory to create either Mock or Wrap api""" - options = options or TEST_CFG + options = options or get_cfg() base_name = os.path.basename(file_name).replace('.pyc', '.py') job_name_prefix = _file_name_subst.sub('', base_name) func_name = None diff --git a/test/framework/cfg/jenkins_security.py b/test/framework/cfg/jenkins_security.py index c5942a3..e1c6923 120000 --- a/test/framework/cfg/jenkins_security.py +++ b/test/framework/cfg/jenkins_security.py @@ -1 +1 @@ -../../../demo/demo_security.py \ No newline at end of file +../../../demo/lib/demo_security.py \ No newline at end of file diff --git a/test/framework/job.xml.tenjin b/test/framework/job.xml.tenjin index abd523d..87e542a 100644 --- a/test/framework/job.xml.tenjin +++ b/test/framework/job.xml.tenjin @@ -78,11 +78,11 @@ import sys sys.path.append("{==test_tmp_dir==}") from jenkinsflow.jobload import update_job_from_template -from jenkinsflow.test.framework.cfg import ApiType +from test.framework.cfg import ApiType - + from jenkinsflow import jenkins_api as jenkins diff --git a/test/framework/killer.py b/test/framework/killer.py index 69b5a5a..5575cbf 100755 --- a/test/framework/killer.py +++ b/test/framework/killer.py @@ -2,9 +2,10 @@ # All rights reserved. This work is under a BSD license, see LICENSE.TXT. import sys, os, signal, time +from pathlib import Path import subprocess -from jenkinsflow.test.framework.logger import log, logt +from .logger import log, logt def _killer(log_file, pid, sleep_time, num_kills): @@ -34,8 +35,9 @@ def kill(api, sleep_time, num_kills): """Kill this process""" pid = os.getpid() log_file_name = api.func_name.replace('test_', '') + ".log" - args = [sys.executable, "-m", "jenkinsflow.test.framework.killer", repr(pid), repr(sleep_time), repr(num_kills), log_file_name] + args = [sys.executable, "-m", f"jenkinsflow.test.framework.{Path(__file__).stem}", + repr(pid), repr(sleep_time), repr(num_kills), log_file_name] with open(log_file_name, 'w') as log_file: logt(log_file, "Invoking kill subprocess.", args) - subprocess.Popen(args) + subprocess.Popen(args, start_new_session=True) diff --git a/test/framework/nox_utils.py b/test/framework/nox_utils.py index 58fb0bd..71ebde9 100644 --- a/test/framework/nox_utils.py +++ b/test/framework/nox_utils.py @@ -4,15 +4,15 @@ import os from pathlib import Path -from typing import Sequence, Tuple, Dict +from typing import Sequence _HERE = Path(__file__).resolve().parent -_TOP_DIR = _HERE.parent.parent +_TOP_DIR = _HERE.parent.parent/"src" from .cfg import ApiType, speedup -def cov_options_env(api_types: Sequence[str], coverage=True) -> Tuple[Sequence[str], Dict[str, str]]: +def cov_options_env(api_types: Sequence[str], coverage=True) -> tuple[Sequence[str], dict[str, str]]: """Setup coverage options. Return pytest coverage options, and env variables dict. @@ -28,9 +28,9 @@ def cov_options_env(api_types: Sequence[str], coverage=True) -> Tuple[Sequence[s elif ApiType.MOCK in api_types and ApiType.SCRIPT in api_types: fail_under = 90 elif ApiType.MOCK in api_types: - fail_under = 88 + fail_under = 86.1 else: - fail_under = 85 + fail_under = 83 # Set coverage exclude lines based on selected API types api_exclude_lines = [] @@ -57,7 +57,7 @@ def cov_options_env(api_types: Sequence[str], coverage=True) -> Tuple[Sequence[s api_exclude_files.append("script_api.py") return ( - [f'--cov={_TOP_DIR}', '--cov-report=term-missing', f'--cov-fail-under={fail_under}', f'--cov-config={_HERE/"coverage_rc"}'], + ['--cov-report=term-missing', f'--cov-fail-under={fail_under}', f'--cov-config={_HERE/"../.coveragerc"}'], { "COV_API_EXCLUDE_LINES": "\n".join(api_exclude_lines), "COV_API_EXCLUDE_FILES": "\n".join(api_exclude_files), diff --git a/test/framework/tmp_install.sh b/test/framework/tmp_install.sh index 7ac047c..9bf5ee6 100755 --- a/test/framework/tmp_install.sh +++ b/test/framework/tmp_install.sh @@ -1,12 +1,9 @@ #!/bin/bash - set -u -source_dir=$(cd $(dirname $0)/../.. && pwd) -target_dir=$1 +source_dir=$1 +target_dir=$2 rsync -a --delete --delete-excluded --exclude .git --exclude '*~' --exclude '*.py[cod]' --exclude '__pycache__' --exclude '*.cache' $source_dir/ $target_dir/ -mkdir -p $target_dir/.cache chmod -R a+r $target_dir chmod a+wrx $(find $target_dir -type d) -chmod a+x $target_dir/{test,demo}/*.py diff --git a/test/set_build_description_test.py b/test/set_build_description_test.py index fc96da3..6f40858 100644 --- a/test/set_build_description_test.py +++ b/test/set_build_description_test.py @@ -260,7 +260,7 @@ def test_set_build_description_cli(api_type, cli_runner, options): _clear_description(api, job) cli_args = [ - 'set_build_description', + 'set-build-description', '--job-name', job.name, '--build-number', repr(build_num), '--description', 'BBB1', @@ -276,7 +276,7 @@ def test_set_build_description_cli(api_type, cli_runner, options): assert _get_description(api, job, build_num) == 'BBB1' cli_args = [ - 'set_build_description', + 'set-build-description', '--job-name', job.name, '--build-number', repr(build_num), '--description', 'BBB2', @@ -309,7 +309,7 @@ def test_set_build_description_cli_env_url(api_type, env_base_url, cli_runner): _clear_description(api, job) cli_args = [ - 'set_build_description', + 'set-build-description', '--job-name', job.name, '--build-number', repr(build_num), '--description', 'BBB1', @@ -339,7 +339,7 @@ def test_set_build_description_cli_no_env_url(api_type, env_no_base_url, cli_run _, _, build_num = job.job_status() cli_args = [ - 'set_build_description', + 'set-build-description', '--job-name', job.name, '--build-number', repr(build_num), '--description', 'BBB1'] @@ -355,7 +355,7 @@ def test_set_build_description_cli_no_env_url(api_type, env_no_base_url, cli_run def test_set_build_description_call_script_help(capfd): # Invoke this in a subprocess to ensure that calling the script works # This will not give coverage as it not not traced through the subprocess call - rc = subprocess.call([sys.executable, jp(_here, '../cli/cli.py'), 'set_build_description', '--help']) + rc = subprocess.call([sys.executable, jp(_here, '../src/cli/cli.py'), 'set-build-description', '--help']) assert rc == 0 sout, _ = capfd.readouterr() diff --git a/test/tmp_install.sh b/test/tmp_install.sh deleted file mode 100755 index 9bf5ee6..0000000 --- a/test/tmp_install.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -u - -source_dir=$1 -target_dir=$2 - -rsync -a --delete --delete-excluded --exclude .git --exclude '*~' --exclude '*.py[cod]' --exclude '__pycache__' --exclude '*.cache' $source_dir/ $target_dir/ -chmod -R a+r $target_dir -chmod a+wrx $(find $target_dir -type d) diff --git a/test/version_test.py b/test/version_test.py new file mode 100644 index 0000000..87a63e9 --- /dev/null +++ b/test/version_test.py @@ -0,0 +1,7 @@ +import re + +import jenkinsflow + + +def test_version_of_properly_installe_package(): + assert re.match(r"[0-9]+\.[0-9]+\.[0-9]+.*", jenkinsflow.__version__)