diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 0000000..dab77c7 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,20 @@ +name: clang-format +on: [push, pull_request] +jobs: + build: + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install clang-format + run: sudo apt install -y cmake clang-format-19 + - name: clang-format + # Create symbolic link to clang-format-19 and + # add it to PATH so that 'clang-format' can + # be used to run 'clang-format-19'. + run: | + ln -s $(which clang-format-19) clang-format + export PATH=$PWD:$PATH + cmake . + make lint diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml new file mode 100644 index 0000000..1f40e94 --- /dev/null +++ b/.github/workflows/cppcheck.yml @@ -0,0 +1,15 @@ +name: cppcheck +on: [push, pull_request] +jobs: + build: + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install cppcheck + run: sudo apt-get install -y cmake cppcheck + - name: Run cppcheck + run: | + cmake . + make check diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..e4f7b77 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,17 @@ +name: ruff +on: [push, pull_request] +jobs: + build: + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install ruff + run: python3 -m pip install --upgrade pip ruff + - name: Run ruff + run: ruff check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57d5113..2fec720 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,3 +14,7 @@ repos: pass_filenames: false always_run: true verbose: true + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.9 + hooks: + - id: ruff-format diff --git a/CMakeLists.txt b/CMakeLists.txt index b44eb9f..3d92f7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,5 +160,5 @@ add_custom_target( add_custom_target( test COMMAND "./tests/test.sh" - DEPENDS cbxp + DEPENDS libcbxp cbxp ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51762aa..f622852 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ The following are a set of guidelines to help you contribute. * [Python Interface Testing](#python-interface-testing) - * [Shell Interface Testing](#shell-interface-testing) + * [CLI Interface Testing](#cli-interface-testing) * [Style Guidelines](#style-guidelines) @@ -48,7 +48,7 @@ There are many ways to contribute to the project. You can write code, work on th If you want to write code, a good way to get started is by looking at the issues section of this repository. Look for the **Good First Issue** tag. Good First Issues are great as a first contribution. ### pre-commit Hooks -To ensure `clang-format` and `cppcheck` are always run against your code on **every commit**, set up the **pre-commit hooks**. +To ensure `clang-format`, `cppcheck`, and `ruff` are always run against your code on **every commit**, set up the **pre-commit hooks**. * Install [`pre-commit`](https://pre-commit.com/). * Setup **pre-commit Hooks**: @@ -97,7 +97,7 @@ CBXP is tested using automated functional tests. Test cases for new functionalit python3 ./tests/test.py ``` -#### Shell Interface Testing +#### CLI Interface Testing 1. Add new tests cases to [`tests/test.sh`](tests/test.sh). 2. Run the test suite using `cmake` and `gmake`. @@ -108,11 +108,11 @@ CBXP is tested using automated functional tests. Test cases for new functionalit ## Style Guidelines -:bulb: _`clang-format` can be setup to run automatically using the [pre-commit Hooks](#pre-commit-hooks)._ +:bulb: _`clang-format` and `ruff` can be setup to run automatically using the [pre-commit Hooks](#pre-commit-hooks)._ -The use of the `clang-format` code formatter is required. +All C/C++ code must be formatted using `clang-format`, and all Python code must be formatted using `ruff`. -The following code style conventions should be followed: +The following C/C++ code style conventions should be followed: * Varible names should use snake case *(i.e., `my_variable`)*. * Pointer variables should start with `p_` *(i.e., `p_my_pointer`)*. * Class variables should end with an `_` to help differentiate between class variables and local function variables *(i.e., `my_class_variable_`)*. @@ -145,10 +145,12 @@ When contributing to CBXP, make sure to complete all applicable tasks in the fol * Make any necessary updates to `pyproject.toml`. * Make any necessary updates to `README.md`. -* Ensure that you have **pre-commit Hooks** setup to ensure that `clang-format` and `cppcheck` are run against the code for every commit you make. +* Ensure that you have **pre-commit Hooks** setup to ensure that `clang-format`, `cppcheck`, and `ruff` are run against the code for every commit you make. +* Check for style violations in C/C++ code by running `gmake lint`. +* Check for style vilotations in Python code by running `ruff check`. +* Format C/C++ code by running `gmake format`. +* Format Python code by running `ruff format`. * Run `cppcheck` static code analysis by running `gmake check`. -* Check for style violations using `clang-format` by running `gmake lint`. -* Format the CBXP code using `clang-format` by running `gmake format`. ## Found a bug? diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..ac9abd3 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,410 @@ +def python_versions +def python_executables_and_wheels_map + +pipeline { + agent { + node { + label 'zOS_Ambitus' + } + } + + parameters { + // Note: Each Python version listed must be installed on the + // build agent and must be added to '$PATH' and '$LIBPATH'. + string ( + name: "pythonVersions", + defaultValue: "", + description: ( + "(Required Always) Comma separated list of Python versions to build " + + "wheels for (i.e., Use '12,13' for Python 3.12 and Python 3.13)." + ) + ) + booleanParam( + name: "createRelease", + defaultValue: false, + description: "Toggle whether or not to create a release from this revision." + ) + string( + name: "releaseTag", + defaultValue: "", + description: ( + "(Required When Creating Releases) This will be " + + "the git tag and version number of the release." + ) + ) + string( + name: "releaseTitle", + defaultValue: "", + description: ( + "(Required When Creating Releases) This will be " + + "the title of the release." + ) + ) + string( + name: "gitHubMilestoneLink", + defaultValue: "", + description: ( + "(Required When Creating Releases) This is the GitHub " + + "Milestone URL that coresponds to the release." + ) + ) + booleanParam( + name: "preRelease", + defaultValue: true, + description: "Toggle whether or not this is a pre-release." + ) + } + + options { + ansiColor('css') + } + + stages { + stage('Parameter Validation') { + steps { + script { + if (params.pythonVersions == "") { + error("'pythonVersions' is a required parameter.") + } + if (params.createRelease) { + if (params.releaseTag == "") { + error("'releaseTag' is a required parameter when creating a release.") + } + if (params.releaseTitle == "") { + error("'releaseTitle' is a required parameter when creating a release.") + } + if (params.gitHubMilestoneLink == "") { + error("'gitHubMilestoneLink' is a required parameter when creating a release.") + } + } + } + } + } + stage('Prepare') { + steps { + clean_python_environment() + sh "cmake ." + } + } + stage('Lint') { + steps { + echo "Linting with clang-format ..." + sh "gmake lint" + } + } + stage('Cppcheck') { + steps { + echo "Running cppcheck ..." + sh "gmake check" + } + } + stage('Create Python Distribution Metadata') { + steps { + script { + python_versions = params.pythonVersions.split(",") + python_executables_and_wheels_map = ( + create_python_executables_and_wheels_map(python_versions) + ) + } + } + } + stage('Install & Function Test') { + steps { + script { + // Python distribution + for (python in python_executables_and_wheels_map.keySet()) { + def wheel = python_executables_and_wheels_map[python]["wheelDefault"] + def tar = python_executables_and_wheels_map[python]["tarPublish"] + + echo "Building '${wheel}' and '${tar}' ..." + sh """ + ${python} -m pip install build>=1.3.0 + ${python} -m build + """ + + echo "Install testing '${wheel}' ..." + sh "${python} -m pip install ./dist/${wheel}" + + echo "Function testing '${wheel}' ..." + sh "${python} tests/test.py" + + clean_python_environment() + + echo "Install testing '${tar}' ..." + sh "${python} -m pip install ./dist/${tar}" + + echo "Function testing '${tar} ..." + sh "${python} tests/test.py" + + clean_python_environment() + clean_git_repo() + } + // CLI/Library distribution + def cbxp_version = get_cbxp_version() + def pax = "cbxp-${cbxp_version}.pax.Z" + echo "Building '${pax}' ..." + sh """ + cmake . + gmake package + """ + + echo "Install testing '${pax}' ..." + sh """ + mkdir install-test + cd install-test + pax -rf ../dist/${pax} + ls -alT cbxp-${cbxp_version}/* + """ + + echo "'Function testing './dist/cbxp' ..." + sh "./tests/test.sh" + + clean_git_repo() + } + } + } + stage('Publish') { + when { + expression { params.createRelease == true } + } + steps { + publish( + python_executables_and_wheels_map, + params.releaseTitle, + params.releaseTag, + env.BRANCH_NAME, + params.gitHubMilestoneLink, + params.preRelease + ) + } + } + } + post { + always { + echo "Cleaning up workspace ..." + cleanWs() + clean_python_environment() + } + } +} + +def create_python_executables_and_wheels_map(python_versions) { + def os = sh( + returnStdout: true, + script: "uname" + ).trim().replace("/", "").toLowerCase() + def zos_release = sh( + returnStdout: true, + script: "uname -r" + ).trim().replace(".", "_") + def processor = sh( + returnStdout: true, + script: "uname -m" + ).trim() + def cbxp_version = get_cbxp_version() + + python_executables_and_wheels_map = [:] + + for (python_version in python_versions) { + python_executables_and_wheels_map["python3.${python_version}"] = [ + "wheelDefault": ( + "cbxp-${cbxp_version}-cp3${python_version}-cp3${python_version}-${os}_${zos_release}_${processor}.whl" + ), + "wheelPublish": "cbxp-${cbxp_version}-cp3${python_version}-none-any.whl", + "tarPublish": "cbxp-${cbxp_version}.tar.gz" + ] + } + + return python_executables_and_wheels_map +} + +def get_cbxp_version() { + return sh( + returnStdout: true, + script: "cat pyproject.toml | grep version | cut -d'=' -f2 | cut -d'\"' -f2" + ).trim() +} + +def clean_python_environment() { + echo "Cleaning Python environment ..." + + sh """ + rm -rf ~/.cache + rm -rf ~/.local + """ +} + +def clean_git_repo() { + echo "Cleaning repo ..." + sh "git clean -fdx" +} + +def publish( + python_executables_and_wheels_map, + release_title, + release_tag, + git_branch, + milestone, + pre_release +) { + if (pre_release == true) { + pre_release = "true" + } + else { + pre_release = "false" + } + withCredentials( + [ + usernamePassword( + credentialsId: 'ambitus-github-access-token', + usernameVariable: 'github_user', + passwordVariable: 'github_access_token' + ) + ] + ) { + + // Creating GitHub releases: + // https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release + // Uploading release assets: + // https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset + + // Use single quotes for credentials since it is the most secure + // method for interpolating secrets according to the Jenkins docs: + // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#string-interpolation + + echo "Creating '${release_title}' GitHub release ..." + + def description = build_description(python_executables_and_wheels_map, release_tag, milestone) + + def release_id = sh( + returnStdout: true, + script: ( + 'curl -f -v -L ' + + '-X POST ' + + '-H "Accept: application/vnd.github+json" ' + + '-H "Authorization: Bearer ${github_access_token}" ' + + '-H "X-GitHub-Api-Version: 2022-11-28" ' + + "https://api.github.com/repos/ambitus/cbxp/releases " + + "-d '{" + + " \"tag_name\": \"${release_tag}\"," + + " \"target_commitish\": \"${git_branch}\"," + + " \"name\": \"${release_title}\"," + + " \"body\": \"${description}\"," + + " \"draft\": false," + + " \"prerelease\": ${pre_release}," + + " \"generate_release_notes\":false" + + "}' | grep '\"id\": ' | head -n1 | cut -d':' -f2 | cut -d',' -f1" + ) + ).trim() + + clean_git_repo() + + def checksums_file = "SHASUMS256.txt.asc" + sh """ + mkdir ./dist + touch ./dist/${checksums_file} + chtag -t -c ISO8859-1 ./dist/${checksums_file} + """ + + def tar_publish + def tar_built = false + + // Build and publish Python packages + for (python in python_executables_and_wheels_map.keySet()) { + def wheel_default = python_executables_and_wheels_map[python]["wheelDefault"] + def wheel_publish = python_executables_and_wheels_map[python]["wheelPublish"] + + echo "Building '${wheel_default}' ..." + + sh """ + ${python} -m pip install build>=1.2.2 + ${python} -m build -w + mv ./dist/${wheel_default} ./dist/${wheel_publish} + """ + + if (tar_built == false) { + tar_publish = python_executables_and_wheels_map[python]["tarPublish"] + echo "Building '${tar_publish}' ..." + sh "${python} -m build -s" + tar_built = true + } + + echo "Uploading '${wheel_default}' as '${wheel_publish}' to '${release_title}' GitHub release ..." + upload_asset(release_id, wheel_publish) + + echo "Adding sha256 checksum for '${wheel_publish}' to ${checksums_file}..." + sh "cd dist && sha256sum -t ${wheel_publish} >> ${checksums_file}" + } + + echo "Uploading '${tar_publish}' to '${release_title}' GitHub release ..." + upload_asset(release_id, tar_publish) + + echo "Adding sha256 checksum for '${tar_publish}' to ${checksums_file}..." + sh "cd dist && sha256sum -t ${tar_publish} >> ${checksums_file}" + + // Build and publish CLI/Library distribution + def cbxp_version = get_cbxp_version() + def pax = "cbxp-${cbxp_version}.pax.Z" + echo "Building '${pax}' ..." + sh """ + cmake . + gmake package + """ + + echo "Uploading '${pax}' to '${release_title}' GitHub release ..." + upload_asset(release_id, pax) + + echo "Adding sha256 checksum for '${pax}' to ${checksums_file}..." + sh "cd dist && sha256sum -t ${pax} >> ${checksums_file}" + + echo "Uploading '${checksums_file}' to '${release_title}' GitHub release ..." + upload_asset(release_id, checksums_file) + } +} + +def upload_asset(release_id, release_asset) { + sh( + 'curl -f -v -L ' + + '-X POST ' + + '-H "Accept: application/vnd.github+json" ' + + '-H "Authorization: Bearer ${github_access_token}" ' + + '-H "X-GitHub-Api-Version: 2022-11-28" ' + + '-H "Content-Type: application/octet-stream" ' + + "\"https://uploads.github.com/repos/ambitus/cbxp/releases/${release_id}/assets?name=${release_asset}\" " + + "--data-binary \"@dist/${release_asset}\"" + ) +} + +def build_description(python_executables_and_wheels_map, release_tag, milestone) { + def description = ( + "Release Notes: ${milestone}\\n" + + "## Python Interface Installation\\n" + ) + + for (python in python_executables_and_wheels_map.keySet()) { + def wheel = python_executables_and_wheels_map[python]["wheelPublish"] + def python_executable = python + def python_label = python.replace("python", "Python ") + description += ( + "Install From **${python_label} Wheel Distribution** *(pre-built)*:\\n" + + "```\\ncurl -O -L https://github.com/ambitus/cbxp/releases/download/${release_tag}/${wheel} " + + "&& ${python_executable} -m pip install ${wheel}\\n```\\n" + ) + } + + def python = python_executables_and_wheels_map.keySet()[-1] + def tar = python_executables_and_wheels_map[python]["tarPublish"] + def cbxp_version = get_cbxp_version() + def pax = "cbxp-${cbxp_version}.pax.Z" + description += ( + "Install From **Source Distribution** *(build on install)*:\\n" + + "> :warning: _Requires z/OS Open XL C/C++ 2.1 compiler._\\n" + + "```\\ncurl -O -L https://github.com/ambitus/cbxp/releases/download/${release_tag}/${tar} " + + "&& python3 -m pip install ${tar}\\n```\\n" + + "## CLI/Library Installation\\n" + + "```\\ncurl -O -L https://github.com/ambitus/cbxp/releases/download/${release_tag}/${pax} " + + "&& pax -rf ${pax}\\n```\\n" + ) + + return description +} diff --git a/README.md b/README.md index 4cd3abd..90a7799 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![clang-format](https://github.com/ambitus/cbxp/actions/workflows/clang-format.yml/badge.svg)](https://github.com/ambitus/cbxp/actions/workflows/clang-format.yml) +[![cppcheck](https://github.com/ambitus/cbxp/actions/workflows/cppcheck.yml/badge.svg)](https://github.com/ambitus/cbxp/actions/workflows/cppcheck.yml) +[![ruff](https://github.com/ambitus/cbxp/actions/workflows/ruff.yml/badge.svg)](https://github.com/ambitus/cbxp/actions/workflows/ruff.yml) [![Version](https://img.shields.io/pypi/v/cbxp?label=alpha)](https://pypi.org/project/cbxp/#history) [![Python Versions](https://img.shields.io/pypi/pyversions/cbxp)](https://pypi.org/project/cbxp/) [![Downloads](https://img.shields.io/pypi/dm/cbxp)](https://pypistats.org/packages/cbxp) @@ -30,7 +33,7 @@ All versions of the **IBM Open Enterprise SDK for Python** that are fully suppor ### Interfaces Currently, the following interfaces are provided for CBXP. Additional interfaces can be added in the future if there are use cases for them. * [Python Interface](https://ambitus.github.io/cbxp/interfaces/python) -* [Shell Interface](https://ambitus.github.io/cbxp/interfaces/shell) +* [CLI Interface](https://ambitus.github.io/cbxp/interfaces/shell) ### Supported Control Blocks diff --git a/pyproject.toml b/pyproject.toml index 8d34536..08210f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools >= 80.9.0"] -build-backend = "setuptools.build_meta" + requires = ["setuptools >= 80.9.0"] + build-backend = "setuptools.build_meta" [project] name="cbxp" @@ -50,3 +50,23 @@ build-backend = "setuptools.build_meta" [tool.setuptools.package-data] cbxp = ["LICENSE", "NOTICES"] + +[tool.ruff.lint] + # Ruff by default is not very aggressive, only enforcing a few rulesets like E and F. + # To ensure well organized code a bunch more rulesets have been enabled + select = [ + "ERA", # Checks for commented out code + "E", + "F", + "B", + "EM", # Error messages and exceptions + "SIM", # Comes with suggestions on simplifying things + "N", # Checks if code conforms to the PEP8 naming conventions + "PLE", + "UP", + "TRY", # Best practices for try-except + "FLY", # Checks for string joins + "I", # Comes with suggestions on organizing imports + "COM", # Checks for missing commas and unnecessary + "A", + ] diff --git a/python/cbxp/_C.pyi b/python/cbxp/_C.pyi index d82dd77..7ab645e 100644 --- a/python/cbxp/_C.pyi +++ b/python/cbxp/_C.pyi @@ -1 +1,5 @@ -def call_cbxp(control_block: str, includes_string: str, debug: bool = False) -> dict: ... +def call_cbxp( # noqa: N999 + control_block: str, + includes_string: str, + debug: bool = False, +) -> dict: ... diff --git a/python/cbxp/__init__.py b/python/cbxp/__init__.py index 99af136..448e3f8 100644 --- a/python/cbxp/__init__.py +++ b/python/cbxp/__init__.py @@ -1,2 +1,2 @@ -from .cbxp import cbxp as cbxp from .cbxp import CBXPError as CBXPError +from .cbxp import cbxp as cbxp diff --git a/python/cbxp/cbxp.py b/python/cbxp/cbxp.py index bd37b4b..49e35e7 100644 --- a/python/cbxp/cbxp.py +++ b/python/cbxp/cbxp.py @@ -3,14 +3,18 @@ from cbxp._C import call_cbxp + class CBXPErrorCode(Enum): """An enum of error and return codes from the cbxp interface""" + COMMA_IN_INCLUDE = -1 BAD_CONTROL_BLOCK = 1 BAD_INCLUDE = 2 + class CBXPError(Exception): """A class of errors for return codes from the cbxp interface""" + def __init__(self, return_code: int, control_block_name: str): self.rc = return_code match self.rc: @@ -24,16 +28,18 @@ def __init__(self, return_code: int, control_block_name: str): message = "an unknown error occurred" super().__init__(message) - + def cbxp( - control_block: str, - includes: list[str] = [], - debug: bool = False + control_block: str, + includes: list[str] = None, + debug: bool = False, ) -> dict: + if includes is None: + includes = [] for include in includes: if "," in include: raise CBXPError(CBXPErrorCode.COMMA_IN_INCLUDE.value, control_block) response = call_cbxp(control_block.lower(), ",".join(includes).lower(), debug=debug) - if response['return_code']: - raise CBXPError(response['return_code'], control_block) - return json.loads(response['result_json']) + if response["return_code"]: + raise CBXPError(response["return_code"], control_block) + return json.loads(response["result_json"]) diff --git a/setup.py b/setup.py index 341d263..d00bc23 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def main(): + [ "cbxp", "externals", - "/usr/include/zos" + "/usr/include/zos", ] ), extra_link_args=[ @@ -37,8 +37,8 @@ def main(): extra_compile_args=[ "-O2", "-fzos-le-char-mode=ascii", - "-Wno-trigraphs" - ] + "-Wno-trigraphs", + ], ), ], "cmdclass": {"build_ext": build_ext}, diff --git a/tests/test.py b/tests/test.py index cf7f2a7..7d3351a 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,8 +1,9 @@ import unittest -from cbxp import cbxp, CBXPError -class TestCBXP(unittest.TestCase): +from cbxp import CBXPError, cbxp + +class TestCBXP(unittest.TestCase): # ============================================================================ # Basic Usage # ============================================================================ @@ -27,12 +28,13 @@ def test_cbxp_can_extract_ascb(self): self.assertIs(type(cbdata), list) for entry in cbdata: self.assertIs(type(entry), dict) - + def test_cbxp_can_extract_assb(self): cbdata = cbxp("assb") self.assertIs(type(cbdata), list) for entry in cbdata: self.assertIs(type(entry), dict) + # ============================================================================ # Include Patterns # ============================================================================ @@ -60,7 +62,7 @@ def test_cbxp_can_extract_the_asvt_and_include_the_ascb(self): self.assertIs(type(cbdata["asvtenty"]), list) for entry in cbdata["asvtenty"]: self.assertIs(type(entry), dict) - + def test_cbxp_can_extract_the_ascb_and_include_the_assb(self): cbdata = cbxp("ascb", includes=["assb"]) self.assertIs(type(cbdata), list) @@ -82,7 +84,7 @@ def test_cbxp_can_extract_the_psa_and_include_the_cvt_ecvt_ascb(self): self.assertIs(type(cbdata["flccvt"]["cvtasvt"]["asvtenty"]), list) for entry in cbdata["flccvt"]["cvtasvt"]["asvtenty"]: self.assertIs(type(entry), dict) - + def test_cbxp_can_extract_the_cvt_and_include_the_asvt_ascb(self): cbdata = cbxp("cvt", includes=["asvt.ascb"]) self.assertIs(type(cbdata), dict) @@ -91,7 +93,7 @@ def test_cbxp_can_extract_the_cvt_and_include_the_asvt_ascb(self): for entry in cbdata["cvtasvt"]["asvtenty"]: self.assertIs(type(entry), dict) - def test_cbxp_include_can_extract_cvt_and_include_pattern_ecvt_and_asvt(self): + def test_cbxp_include_can_extract_cvt_and_include_ecvt_and_asvt(self): cbdata = cbxp("cvt", includes=["ecvt", "asvt"]) self.assertIs(type(cbdata), dict) self.assertIs(type(cbdata["cvtecvt"]), dict) @@ -100,7 +102,9 @@ def test_cbxp_include_can_extract_cvt_and_include_pattern_ecvt_and_asvt(self): for entry in cbdata["cvtasvt"]["asvtenty"]: self.assertIs(type(entry), str) - def test_cbxp_include_can_extract_psa_and_include_pattern_ecvt_asvt_and_cvt_asvt_ascb(self): + def test_cbxp_include_can_extract_psa_and_include_ecvt_asvt_and_cvt_asvt_ascb( + self, + ): cbdata = cbxp("psa", includes=["cvt.ecvt", "cvt.asvt.ascb"]) self.assertIs(type(cbdata), dict) self.assertIs(type(cbdata["flccvt"]), dict) @@ -109,8 +113,10 @@ def test_cbxp_include_can_extract_psa_and_include_pattern_ecvt_asvt_and_cvt_asvt self.assertIs(type(cbdata["flccvt"]["cvtasvt"]["asvtenty"]), list) for entry in cbdata["flccvt"]["cvtasvt"]["asvtenty"]: self.assertIs(type(entry), dict) - - def test_cbxp_include_can_extract_psa_and_include_pattern_ecvt_asvt_and_cvt_asvt_ascb_assb(self): + + def test_cbxp_include_can_extract_psa_and_include_ecvt_asvt_and_cvt_asvt_ascb_assb( + self, + ): cbdata = cbxp("psa", includes=["cvt.ecvt", "cvt.asvt.ascb.assb"]) self.assertIs(type(cbdata), dict) self.assertIs(type(cbdata["flccvt"]), dict) @@ -141,7 +147,7 @@ def test_cbxp_can_extract_psa_and_include_cvt_wildcard(self): self.assertIs(type(cbdata["flccvt"]["cvtasvt"]["asvtenty"]), list) for entry in cbdata["flccvt"]["cvtasvt"]["asvtenty"]: self.assertIs(type(entry), str) - + def test_cbxp_can_extract_cvt_and_include_wildcard_and_asvt_wildcard(self): cbdata = cbxp("cvt", includes=["*", "asvt.*"]) self.assertIs(type(cbdata), dict) @@ -150,8 +156,10 @@ def test_cbxp_can_extract_cvt_and_include_wildcard_and_asvt_wildcard(self): self.assertIs(type(cbdata["cvtasvt"]["asvtenty"]), list) for entry in cbdata["cvtasvt"]["asvtenty"]: self.assertIs(type(entry), dict) - - def test_cbxp_can_extract_cvt_and_include_wildcard_and_asvt_recursive_wildcard(self): + + def test_cbxp_can_extract_cvt_and_include_wildcard_and_asvt_recursive_wildcard( + self, + ): cbdata = cbxp("cvt", includes=["*", "asvt.**"]) self.assertIs(type(cbdata), dict) self.assertIs(type(cbdata["cvtecvt"]), dict) @@ -174,49 +182,58 @@ def test_cbxp_can_run_in_debug_mode(self): def test_cbxp_raises_cbxp_error_when_unknown_control_block_is_provided(self): with self.assertRaises(CBXPError) as e: cbxp("unknown") - self.assertEqual("Unknown control block 'unknown' was specified.", str(e.exception)) + self.assertEqual( + "Unknown control block 'unknown' was specified.", + str(e.exception), + ) # ============================================================================ # Errors: Bad Include Patterns # ============================================================================ - def test_cbxp_raises_cbxp_error_if_asvt_ascb_is_included_when_extracting_the_psa(self): + def test_cbxp_raises_cbxp_error_if_asvt_ascb_is_included_with_the_psa( + self, + ): with self.assertRaises(CBXPError) as e: cbxp("psa", includes=["asvt.ascb"]) self.assertEqual("A bad include pattern was provided", str(e.exception)) - def test_cbxp_raises_cbxp_error_if_ascb_is_included_when_extracting_the_psa(self): + def test_cbxp_raises_cbxp_error_if_ascb_is_included_with_the_psa(self): with self.assertRaises(CBXPError) as e: cbxp("psa", includes=["ascb"]) self.assertEqual("A bad include pattern was provided", str(e.exception)) - def test_cbxp_raises_cbxp_error_if_ecvt_is_included_when_extracting_the_ascb(self): + def test_cbxp_raises_cbxp_error_if_ecvt_is_included_with_ascb(self): with self.assertRaises(CBXPError) as e: cbxp("ascb", includes=["ecvt"]) self.assertEqual("A bad include pattern was provided", str(e.exception)) - - def test_cbxp_raises_cbxp_error_if_ect_ecvt_and_cvt_ascb_is_included_when_extracting_the_psa(self): + + def test_cbxp_raises_cbxp_error_if_ect_ecvt_and_cvt_ascb_is_included_with_the_psa( + self, + ): with self.assertRaises(CBXPError) as e: - cbdata = cbxp("psa", includes=["cvt.ecvt", "cvt.ascb"]) + cbxp("psa", includes=["cvt.ecvt", "cvt.ascb"]) self.assertEqual("A bad include pattern was provided", str(e.exception)) - def test_cbxp_raises_cbxp_error_if_ecvt_and_cvt_asvt_ascb_is_included_when_extracting_the_psa(self): + def test_cbxp_raises_cbxp_error_if_ecvt_and_cvt_asvt_ascb_is_included_with_the_psa( + self, + ): with self.assertRaises(CBXPError) as e: - cbdata = cbxp("psa", includes=["ecvt", "cvt.asvt.ascb"]) + cbxp("psa", includes=["ecvt", "cvt.asvt.ascb"]) self.assertEqual("A bad include pattern was provided", str(e.exception)) - def test_cbxp_raises_cbxp_error_if_cvt_is_included_when_extracting_the_cvt(self): + def test_cbxp_raises_cbxp_error_if_cvt_is_included_with_the_cvt(self): with self.assertRaises(CBXPError) as e: - cbdata = cbxp("cvt", includes=["cvt"]) + cbxp("cvt", includes=["cvt"]) self.assertEqual("A bad include pattern was provided", str(e.exception)) def test_cbxp_raises_cbxp_error_when_pattern_cannot_contain_comma_1(self): with self.assertRaises(CBXPError) as e: - cbdata = cbxp("cvt", includes=["asvt,ascb"]) + cbxp("cvt", includes=["asvt,ascb"]) self.assertEqual("Include patterns cannot contain commas", str(e.exception)) def test_cbxp_raises_cbxp_error_when_pattern_cannot_contain_comma_2(self): with self.assertRaises(CBXPError) as e: - cbdata = cbxp("cvt", includes=["asvt,as"]) + cbxp("cvt", includes=["asvt,as"]) self.assertEqual("Include patterns cannot contain commas", str(e.exception))