pax_global_header00006660000000000000000000000064146355135210014517gustar00rootroot0000000000000052 comment=de0c447d50ef4d8fa12d20af6c932bd8bcff0021 pusimp-0.1.1/000077500000000000000000000000001463551352100130335ustar00rootroot00000000000000pusimp-0.1.1/.github/000077500000000000000000000000001463551352100143735ustar00rootroot00000000000000pusimp-0.1.1/.github/FUNDING.yml000066400000000000000000000000331463551352100162040ustar00rootroot00000000000000github: francesco-ballarin pusimp-0.1.1/.github/workflows/000077500000000000000000000000001463551352100164305ustar00rootroot00000000000000pusimp-0.1.1/.github/workflows/ci.yml000066400000000000000000000167651463551352100175650ustar00rootroot00000000000000name: CI on: push: branches: - "**" pull_request: branches: - main schedule: - cron: "0 0 * * WED" workflow_dispatch: inputs: index: description: "The package index, e.g. PyPI or TestPyPI, from which to install the package. If empty, the package will not be installed from any package index, but from the current git clone" index_version: description: "The version of the package to be installed from the package index. If empty, the latest version will be installed. Only used when index is non empty." workflow_call: inputs: ref: description: "The branch, tag or SHA to checkout" type: string index: description: "The package index, e.g. PyPI or TestPyPI, from which to install the package. If empty, the package will not be installed from any package index, but from the current git clone" type: string index_version: description: "The version of the package to be installed from the package index. If empty, the latest version will be installed. Only used when index is non empty." type: string jobs: test: runs-on: ubuntu-latest strategy: matrix: include: - name: python-3.8 python-version: "3.8" - name: python-3.9 python-version: "3.9" - name: python-3.10 python-version: "3.10" - name: python-3.11 python-version: "3.11" - name: python-3.12 python-version: "3.12" - name: debian container: debian:testing - name: conda container: condaforge/miniforge3 fail-fast: false name: ${{ matrix.name }} container: ${{ matrix.container }} steps: - name: Install git if: matrix.name == 'debian' run: | apt update -y -q apt install -y -qq git - name: Mark workspace as safe run: | git config --global --add safe.directory "${GITHUB_WORKSPACE}" - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} - name: Install dependencies (default GitHub actions image) if: startsWith(matrix.name, 'python-') == true uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies (debian image) if: matrix.name == 'debian' run: | export DEBIAN_FRONTEND="noninteractive" apt -y -q update apt install -y -qq python3-pip python3-virtualenv - name: Install dependencies (conda image) if: matrix.name == 'conda' run: | conda install -y pip virtualenv - name: Determine default flags for pip install id: determine_pip_install_default_flags run: | if [[ "${{ matrix.name }}" == "debian" ]]; then echo "pip_install_default_flags=--break-system-packages" >> ${GITHUB_OUTPUT} else echo "pip_install_default_flags=" >> ${GITHUB_OUTPUT} fi shell: bash - name: Wait for package index availability (PyPI and TestPyPI only) if: (inputs || github.event.inputs).index != '' && (inputs || github.event.inputs).index_version != '' run: | INDEX=${{ (inputs || github.event.inputs).index }} INDEX_VERSION=${{ (inputs || github.event.inputs).index_version }} PIP_INSTALL_DEFAULT_FLAGS=${{ steps.determine_pip_install_default_flags.outputs.pip_install_default_flags }} PACKAGE_NAME="pusimp" if [[ "${INDEX}" == "TestPyPI" ]]; then INDEX_URL=https://test.pypi.org elif [[ "${INDEX}" == "PyPI" ]]; then INDEX_URL=https://pypi.org else echo "Invalid package index" && exit 1 fi COUNTER=0 INDEX_VERSION_FOUND=0 while [[ ${INDEX_VERSION_FOUND} -ne 1 ]]; do pip install ${PIP_INSTALL_DEFAULT_FLAGS} --no-cache-dir --index-url ${INDEX_URL}/simple/ ${PACKAGE_NAME}== 2> all_${PACKAGE_NAME}_versions || true if grep -q ${INDEX_VERSION} all_${PACKAGE_NAME}_versions; then INDEX_VERSION_FOUND=1 fi [[ ${INDEX_VERSION_FOUND} -ne 1 && ${COUNTER} -eq 5 ]] && echo "Giving up on finding version ${INDEX_VERSION} on ${INDEX_URL}" && exit 1 [[ ${INDEX_VERSION_FOUND} -ne 1 ]] && echo "Cannot find version ${INDEX_VERSION} on ${INDEX_URL}, attempt ${COUNTER}: trying again after a short pause" && sleep 10 [[ ${INDEX_VERSION_FOUND} -eq 1 ]] && echo "Found version ${INDEX_VERSION} on ${INDEX_URL}, attempt ${COUNTER}" COUNTER=$((COUNTER+1)) done shell: bash - name: Install pusimp (PyPI and TestPyPI only) if: (inputs || github.event.inputs).index != '' run: | INDEX=${{ (inputs || github.event.inputs).index }} INDEX_VERSION=${{ (inputs || github.event.inputs).index_version }} PIP_INSTALL_DEFAULT_FLAGS=${{ steps.determine_pip_install_default_flags.outputs.pip_install_default_flags }} PACKAGE_NAME="pusimp" if [[ "${INDEX}" == "TestPyPI" ]]; then INDEX_FLAGS="--no-cache-dir --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/" elif [[ "${INDEX}" == "PyPI" ]]; then INDEX_FLAGS="--no-cache-dir" else echo "Invalid package index" && exit 1 fi if [[ -n "${INDEX_VERSION}" ]]; then PACKAGE_VERSION="==${INDEX_VERSION}" echo "Installing version ${INDEX_VERSION} from ${INDEX}" else PACKAGE_VERSION="" echo "Installing latest version from ${INDEX}" fi python3 -m pip install ${PIP_INSTALL_DEFAULT_FLAGS} ${INDEX_FLAGS} ${PACKAGE_NAME}[lint,tests]${PACKAGE_VERSION} shell: bash - name: Install pusimp (git clone only) if: (inputs || github.event.inputs).index == '' run: | PIP_INSTALL_DEFAULT_FLAGS=${{ steps.determine_pip_install_default_flags.outputs.pip_install_default_flags }} python3 -m pip install ${PIP_INSTALL_DEFAULT_FLAGS} .[lint,tests] - name: Install pusimp test data run: | PIP_INSTALL_DEFAULT_FLAGS=${{ steps.determine_pip_install_default_flags.outputs.pip_install_default_flags }} find tests/data -mindepth 1 -maxdepth 1 -type d -exec python3 -m pip install ${PIP_INSTALL_DEFAULT_FLAGS} {} \; - name: Clean build files run: | git clean -xdf - name: Run ruff run: | python3 -m ruff . - name: Run isort run: | python3 -m isort --check --diff . - name: Run mypy run: | python3 -m mypy . - name: Run yamllint run: | python3 -m yamllint -d "{extends: default, rules: {document-start: {present: false}, line-length: disable, truthy: {check-keys: false}}}" . - name: Run tests run: | python3 -m coverage run --source=pusimp -m pytest -vv tests/unit - name: Check test coverage run: | python3 -m coverage report --fail-under=100 --show-missing --skip-covered warn: runs-on: ubuntu-latest if: github.repository == 'python-pusimp/pusimp' && github.ref == 'refs/heads/main' && github.event_name == 'schedule' steps: - name: Warn if scheduled workflow is about to be disabled uses: fem-on-colab/warn-workflow-about-to-be-disabled-action@main with: workflow-filename: ci.yml days-elapsed: 50 pusimp-0.1.1/.github/workflows/ci_against_releases.yml000066400000000000000000000021531463551352100231400ustar00rootroot00000000000000name: CI (against releases) on: schedule: - cron: "0 0 * * WED" workflow_dispatch: workflow_call: jobs: test_0_1_0_installing_from_github: uses: python-pusimp/pusimp/.github/workflows/ci.yml@v0.1.0.post1 with: ref: v0.1.0 test_0_1_0_installing_from_pypi: uses: python-pusimp/pusimp/.github/workflows/ci.yml@v0.1.0.post1 with: ref: v0.1.0 index: PyPI index_version: 0.1.0 test_0_1_1_installing_from_github: uses: python-pusimp/pusimp/.github/workflows/ci.yml@v0.1.1 with: ref: v0.1.1 test_0_1_1_installing_from_pypi: uses: python-pusimp/pusimp/.github/workflows/ci.yml@v0.1.1 with: ref: v0.1.1 index: PyPI index_version: 0.1.1 warn: runs-on: ubuntu-latest if: github.repository == 'python-pusimp/pusimp' && github.ref == 'refs/heads/main' && github.event_name == 'schedule' steps: - name: Warn if scheduled workflow is about to be disabled uses: fem-on-colab/warn-workflow-about-to-be-disabled-action@main with: workflow-filename: ci_against_releases.yml days-elapsed: 50 pusimp-0.1.1/.github/workflows/pypi.yml000066400000000000000000000132341463551352100201370ustar00rootroot00000000000000name: "Publish on PyPI (internal: use 'Release new version' instead)" on: schedule: - cron: "0 0 * * WED" workflow_dispatch: inputs: index: description: "The package index, e.g. PyPI or TestPyPI. Defaults to TestPyPI. Be careful when choosing PyPI, because uploads there cannot be deleted" workflow_call: inputs: ref: description: "The branch, tag or SHA to checkout" type: string index: description: "The package index, e.g. PyPI or TestPyPI. Defaults to TestPyPI. Be careful when choosing PyPI, because uploads there cannot be deleted" type: string secrets: PYPI_TOKEN: description: "Token that enables publishing to PyPI" TEST_PYPI_TOKEN: description: "Token that enables publishing to TestPyPI" jobs: process_inputs: runs-on: ubuntu-latest steps: - name: Determine package index id: determine_index run: | if [[ -n "${{ (inputs || github.event.inputs).index }}" ]]; then echo "index=${{ (inputs || github.event.inputs).index }}" >> ${GITHUB_OUTPUT} else echo "index=TestPyPI" >> ${GITHUB_OUTPUT} fi shell: bash outputs: index: ${{ steps.determine_index.outputs.index }} build_distributions: needs: [process_inputs] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} - name: Add current date at the end of the version string (TestPyPI only) if: needs.process_inputs.outputs.index == 'TestPyPI' run: | DATETIME=$(date "+%Y%m%d%H%M%S") sed -i -r "s|version = \"(.*)\"|version = \"\19999${DATETIME}\"|g" pyproject.toml - name: Build distributions run: pipx run build - name: Determine distribution version id: determine_version run: | python3 -m pip install wheel-filename WHEELS=($(find dist -type f -name "*.whl")) if [[ "${#WHEELS[@]}" == "1" ]]; then VERSION=$(python3 -c "import wheel_filename; print(wheel_filename.parse_wheel_filename('${WHEELS[0]}').version)") echo "version=${VERSION}" >> ${GITHUB_OUTPUT} else echo "Found ${#WHEELS[@]} wheels, instead of one" && exit 1 fi shell: bash - name: Upload distributions as an artifact uses: actions/upload-artifact@v4 with: name: distributions-${{ steps.determine_version.outputs.version }} path: dist/ - name: Verify distributions metadata run: pipx run twine check dist/* outputs: version: ${{ steps.determine_version.outputs.version }} publish: needs: [process_inputs, build_distributions] runs-on: ubuntu-latest steps: - name: Determine package index token id: determine_index_token run: | INDEX=${{ needs.process_inputs.outputs.index }} if [[ "${INDEX}" == "PyPI" ]]; then INDEX_TOKEN=${{ secrets.PYPI_TOKEN }} elif [[ "${INDEX}" == "TestPyPI" ]]; then INDEX_TOKEN=${{ secrets.TEST_PYPI_TOKEN }} else echo "Invalid package index" && exit 1 fi if [[ -n "${INDEX_TOKEN}" ]]; then echo "index_token=${INDEX_TOKEN}" >> ${GITHUB_OUTPUT} else echo "Missing package index token" && exit 1 fi - name: Determine package index repository url id: determine_index_repository_url run: | INDEX=${{ needs.process_inputs.outputs.index }} if [[ "${INDEX}" == "PyPI" ]]; then echo "index_repository_url=" >> ${GITHUB_OUTPUT} elif [[ "${INDEX}" == "TestPyPI" ]]; then echo "index_repository_url=https://test.pypi.org/legacy/" >> ${GITHUB_OUTPUT} else echo "Invalid package index" && exit 1 fi - name: Report version and index which will be used run: | echo "Publishing version ${{ needs.build_distributions.outputs.version }} on ${{ needs.process_inputs.outputs.index }} (index repository URL: ${{ steps.determine_index_repository_url.outputs.index_repository_url }})." - name: Download distributions from artifacts uses: actions/download-artifact@v4 with: name: distributions-${{ needs.build_distributions.outputs.version }} path: dist - name: Disallow publishing development versions (PyPI only) if: needs.process_inputs.outputs.index == 'PyPI' run: | VERSION=${{ needs.build_distributions.outputs.version }} if [[ ${VERSION} == *"dev"* ]]; then echo "Cannot publish development version ${VERSION} on PyPI" && exit 1 fi shell: bash - name: Publish package distributions uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ steps.determine_index_token.outputs.index_token }} repository-url: ${{ steps.determine_index_repository_url.outputs.index_repository_url }} test: needs: [process_inputs, build_distributions, publish] uses: python-pusimp/pusimp/.github/workflows/ci.yml@main with: ref: ${{ inputs.ref }} index: ${{ needs.process_inputs.outputs.index }} index_version: ${{ needs.build_distributions.outputs.version }} warn: runs-on: ubuntu-latest if: github.repository == 'python-pusimp/pusimp' && github.ref == 'refs/heads/main' && github.event_name == 'schedule' steps: - name: Warn if scheduled workflow is about to be disabled uses: fem-on-colab/warn-workflow-about-to-be-disabled-action@main with: workflow-filename: pypi.yml days-elapsed: 50 pusimp-0.1.1/.github/workflows/release.yml000066400000000000000000000200671463551352100206000ustar00rootroot00000000000000name: Release new version on: workflow_dispatch: inputs: version: description: "Version number for the release, without v prefix" next_version: description: "Version number for the next release, without v prefix and without dev" dry_run: description: "Dry-run: if 'no', publish to PyPI, create a tag and merge to the calling branch; if 'yes', do a dry-run where none of the above tasks are executed. If empty, its default value is 'yes' and a dry-run is carried out." jobs: prepare_branches: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: token: ${{ secrets.REPO_ACCESS_TOKEN }} - name: Configure username and email run: | git config user.name "GitHub Actions" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Check consistency with current development version run: | python3 -m pip install toml DEV_VERSION=$(python3 -c 'import toml; print(toml.load("pyproject.toml")["project"]["version"])') VERSION=${{ github.event.inputs.version }} if [[ "${DEV_VERSION/dev/}" != "${VERSION}" ]]; then echo "Current development version is ${DEV_VERSION}, while the requested version number for the upcoming release is ${VERSION}. The two must match, apart from 'dev', but they do not." && exit 1 fi shell: bash - name: Check consistency between version and next version run: | VERSION=${{ github.event.inputs.version }} NEXT_VERSION=${{ github.event.inputs.next_version }} IFS=. read -a VERSION_PARTS <<< ${VERSION} if [[ "${#VERSION_PARTS[@]}" != "3" ]]; then echo "Expected major.minor.micro format, got ${VERSION}" && exit fi NEXT_MAJOR_VERSION=$((${VERSION_PARTS[0]} + 1)).${VERSION_PARTS[1]}.${VERSION_PARTS[2]} NEXT_MINOR_VERSION=${VERSION_PARTS[0]}.$((${VERSION_PARTS[1]} + 1)).${VERSION_PARTS[2]} NEXT_MICRO_VERSION=${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.$((${VERSION_PARTS[2]} + 1)) if [[ "${NEXT_VERSION}" != "${NEXT_MAJOR_VERSION}" && "${NEXT_VERSION}" != "${NEXT_MINOR_VERSION}" && "${NEXT_VERSION}" != "${NEXT_MICRO_VERSION}" ]]; then echo "Expected next version to be either ${NEXT_MAJOR_VERSION}, ${NEXT_MINOR_VERSION} or ${NEXT_MICRO_VERSION}, while the requested version number for the next release is ${NEXT_VERSION}." && exit 1 fi shell: bash - name: Verify that there is no tag with the same version number run: | VERSION=${{ github.event.inputs.version }} if [[ -n $(git ls-remote --tags origin v${VERSION}) ]]; then echo "A v${VERSION} tag already exists" && exit 1 fi shell: bash - name: Delete existing branches run: | VERSION=${{ github.event.inputs.version }} for BRANCH_NAME in pre${VERSION} v${VERSION} post${VERSION}; do if [[ -n $(git branch --list ${BRANCH_NAME}) ]]; then git branch -D ${BRANCH_NAME} fi if [[ -n $(git ls-remote --heads origin ${BRANCH_NAME}) ]]; then git push origin -d ${BRANCH_NAME} fi done shell: bash - name: Prepare branch pre release run: | VERSION=${{ github.event.inputs.version }} PRE_BRANCH_NAME=pre${VERSION} git checkout -b ${PRE_BRANCH_NAME} git push origin ${PRE_BRANCH_NAME} - name: Prepare branch for release run: | VERSION=${{ github.event.inputs.version }} VERSION_DASHES=${VERSION//./_} RELEASE_BRANCH_NAME=v${VERSION} git checkout -b ${RELEASE_BRANCH_NAME} find . -type f -name pyproject.toml -exec sed -i "s|version = \".*\"|version = \"${VERSION}\"|g" {} \; CI_AGAINST_VERSION=$(cat << EOF test_${VERSION_DASHES}_installing_from_github: uses: python-pusimp/pusimp/.github/workflows/ci.yml@v${VERSION} with: ref: v${VERSION} test_${VERSION_DASHES}_installing_from_pypi: uses: python-pusimp/pusimp/.github/workflows/ci.yml@v${VERSION} with: ref: v${VERSION} index: PyPI index_version: ${VERSION} warn: EOF ) sed -i "s| warn:|${CI_AGAINST_VERSION//$'\n'/\\n}|" .github/workflows/ci_against_releases.yml git add . git commit -m "Bump version to ${VERSION}" git push origin ${RELEASE_BRANCH_NAME} shell: bash - name: Prepare branch post release run: | VERSION=${{ github.event.inputs.version }} NEXT_VERSION=${{ github.event.inputs.next_version }} POST_BRANCH_NAME=post${VERSION} NEXT_DEV_VERSION=$(echo ${NEXT_VERSION} | awk -F. -v OFS=. '{$NF="dev"$NF;print}') git checkout -b ${POST_BRANCH_NAME} find . -type f -name pyproject.toml -exec sed -i "s|version = \"${VERSION}\"|version = \"${NEXT_DEV_VERSION}\"|g" {} \; git add . git commit -m "Reset version number to ${NEXT_DEV_VERSION} after release" git push origin ${POST_BRANCH_NAME} test_branch_pre_release_installing_from_github: needs: [prepare_branches] uses: python-pusimp/pusimp/.github/workflows/ci.yml@main with: ref: pre${{ github.event.inputs.version }} test_branch_for_release_installing_from_github: needs: [prepare_branches] uses: python-pusimp/pusimp/.github/workflows/ci.yml@main with: ref: v${{ github.event.inputs.version }} test_branch_post_release_installing_from_github: needs: [prepare_branches] uses: python-pusimp/pusimp/.github/workflows/ci.yml@main with: ref: post${{ github.event.inputs.version }} publish_release_to_testpypi: needs: [test_branch_pre_release_installing_from_github, test_branch_for_release_installing_from_github, test_branch_post_release_installing_from_github] uses: python-pusimp/pusimp/.github/workflows/pypi.yml@main with: ref: v${{ github.event.inputs.version }} index: TestPyPI secrets: TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }} publish_release_to_pypi: needs: [publish_release_to_testpypi] if: github.event.inputs.dry_run == 'no' uses: python-pusimp/pusimp/.github/workflows/pypi.yml@main with: ref: v${{ github.event.inputs.version }} index: PyPI secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} push_to_repo_and_cleanup: needs: [publish_release_to_pypi] if: github.event.inputs.dry_run == 'no' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.REPO_ACCESS_TOKEN }} - name: Delete branch pre release run: | VERSION=${{ github.event.inputs.version }} PRE_BRANCH_NAME=pre${VERSION} git push origin -d ${PRE_BRANCH_NAME} - name: Replace branch for release with a tag run: | VERSION=${{ github.event.inputs.version }} RELEASE_BRANCH_NAME=v${VERSION} BACKUP_HEAD=$(git rev-parse HEAD) git checkout ${RELEASE_BRANCH_NAME} RELEASE_BRANCH_NAME_HEAD=$(git rev-parse HEAD) git checkout ${BACKUP_HEAD} git branch -D ${RELEASE_BRANCH_NAME} git push origin -d ${RELEASE_BRANCH_NAME} git tag ${RELEASE_BRANCH_NAME} ${RELEASE_BRANCH_NAME_HEAD} git push origin ${RELEASE_BRANCH_NAME} - name: Merge branch post release with the branch from which this workflow was called run: | VERSION=${{ github.event.inputs.version }} POST_BRANCH_NAME=post${VERSION} CALLING_BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} git checkout ${POST_BRANCH_NAME} git checkout ${CALLING_BRANCH_NAME} git merge --ff-only ${POST_BRANCH_NAME} git branch -D ${POST_BRANCH_NAME} git push origin -d ${POST_BRANCH_NAME} git push origin ${CALLING_BRANCH_NAME} pusimp-0.1.1/.gitignore000066400000000000000000000001171463551352100150220ustar00rootroot00000000000000*~ *.bak *.cache *.egg *.egg-info *.py[cod] .coverage .pytest_cache build dist pusimp-0.1.1/AUTHORS000066400000000000000000000001261463551352100141020ustar00rootroot00000000000000Francesco Ballarin Drew Parsons pusimp-0.1.1/LICENSE000066400000000000000000000020641463551352100140420ustar00rootroot00000000000000Copyright 2023-2024 pusimp authors and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pusimp-0.1.1/README.md000066400000000000000000000120371463551352100143150ustar00rootroot00000000000000# pusimp - prevent user-site imports **pusimp** is a python library to prevent user-site imports of specific dependencies of a package. The typical scenario for using **pusimp** is in combination with a system manager (e.g., `apt` for Debian), to prevent dependencies from being loaded from user-site instead of the location provided by the system manager. **pusimp** is currently developed and maintained at [Università Cattolica del Sacro Cuore](https://www.unicatt.it/) by [Prof. Francesco Ballarin](https://www.francescoballarin.it), in collaboration with [Prof. Drew Parsons](https://web.unica.it/unica/page/en/drewf_parsons) at [Università degli Studi di Cagliari](https://www.unica.it/). ## The acronym **pusimp** is an acronym for "**p**revent **u**ser-**s**ite **imp**orts". However, an internet search reveals that PUSIMP is also a slang term that stands for "Put yourself in my position". In agreement with the slang meaning, **pusimp** reports an informative (although, arguably, quite long) error message to guide the user towards solving the conflict in their dependencies. ## Content The logic of **pusimp** is implemented in [a single python file](https://github.com/python-pusimp/pusimp/blob/main/pusimp/prevent_user_site_imports.py), which exposes the function `pusimp.prevent_user_site_imports`. **pusimp** can be `pip install`ed from [its GitHub repository](https://github.com/python-pusimp/pusimp/) or from [PyPI](https://pypi.org/project/pusimp/). ## Sample usage Assume to be the maintainer of a package named `my_package`, with website `https://www.my.package`. `my_package` depends on the auxiliary packages `my_dependency_one`, `my_dependency_two`, `my_dependency_three`, and optionally on `my_dependency_four`. Furthermore, assume that all five packages are installed by the system manager `my_apt` at the path `/usr/lib/python3.xy/site-packages`, and that the four dependencies are available on `pypi` as `my-dependency-one`, `my-dependency-two`, `my-dependency-three`, and `my-dependency-four`. The corresponding sample usage in this case is: ``` import pusimp pusimp.prevent_user_site_imports( "my_package", "my_apt", "https://www.my.package", "/usr/lib/python3.xy/site-packages", ["my_dependency_one", "my_dependency_two", "my_dependency_three", "my_dependency_four"], ["my-dependency-one", "my-dependency-two", "my-dependency-three", "my-dependency-four"], [False, False, False, True], [ "Additional message for my_dependency_one.", "", "", "Maybe inform the user that my_dependency_four is optional." ], lambda executable, dependency_pypi_name, dependency_actual_path: f"{executable} -m pip uninstall {dependency_pypi_name}" ) ``` Suppose now to have a broken configuration in which `my_dependency_one` is missing, `my_dependency_two` is broken, while `my_dependency_three` and `my_dependency_four` are installed on the user-site location. A sample error in such case is the following (the terminal will automatically handle line wrapping of long lines): ``` pusimp has detected the following problems with my_package dependencies: 1) Missing dependencies: * my_dependency_one is missing. Its expected path was /usr/lib/python3.xy/site-packages/my_dependency_one/__init__.py. 2) Broken dependencies: * my_dependency_two is broken. Error on import was 'purposely broken'. 3) Dependencies imported from a local path rather than from the path provided by my_apt: * my_dependency_three was imported from a local path: expected in /usr/lib/python3.xy/site-packages/my_dependency_three/__init__.py, but imported from ~/.local/lib/python3.xy/site-packages/my_dependency_three/__init__.py. * my_dependency_four was imported from a local path: expected in /usr/lib/python3.xy/site-packages/my_dependency_four/__init__.py, but imported from ~/.local/lib/python3.xy/site-packages/my_dependency_four/__init__.py. pusimp suggests to apply all of the following fixes: 1) To install missing dependencies: * check how to install my_dependency_one with my_apt. 2) To fix broken dependencies: * run 'python3 -m pip show my-dependency-two' in a terminal: if the location field is not /usr/lib/python3.xy/site-packages consider running 'python3 -m pip uninstall my-dependency-two' in a terminal, because the broken dependency is probably being imported from a local path rather than from the path provided by my_apt. 3) To uninstall local dependencies: * run 'python3 -m pip uninstall my-dependency-three' in a terminal, and verify that you are prompted to confirm removal of files in ~/.local/lib/python3.xy/site-packages/my_dependency_three. * run 'python3 -m pip uninstall my-dependency-four' in a terminal, and verify that you are prompted to confirm removal of files in ~/.local/lib/python3.xy/site-packages/my_dependency_four. Maybe inform the user that my_dependency_four is optional. You can disable this check by exporting the MY_PACKAGE_ALLOW_USER_SITE_IMPORTS environment variable. Note, however, that this may break the installation provided by my_apt. If you believe that this message appears incorrectly, report this at https://www.my.package . ``` pusimp-0.1.1/pusimp/000077500000000000000000000000001463551352100143505ustar00rootroot00000000000000pusimp-0.1.1/pusimp/__init__.py000066400000000000000000000003731463551352100164640ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Main module file.""" from pusimp.prevent_user_site_imports import prevent_user_site_imports __all__ = ["prevent_user_site_imports"] pusimp-0.1.1/pusimp/prevent_user_site_imports.py000066400000000000000000000246421463551352100222540ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Prevent user-site imports on a specific set of dependencies.""" import importlib import os import sys import typing def prevent_user_site_imports( package_name: str, system_manager: str, contact_url: str, dependencies_expected_prefix: str, dependencies_import_name: typing.List[str], dependencies_pypi_name: typing.List[str], dependencies_optional: typing.List[bool], dependencies_extra_error_message: typing.List[str], pip_uninstall_call: typing.Callable[[str, str, str], str] ) -> None: """ Prevent user-site imports on a specific set of dependencies. Parameters ---------- package_name The name of the package which dependencies must be guarded against user-site imports. This information is only employed to prepare the text of error messages. system_manager The name of the system manager with which the package was installed. This information is only employed to prepare the text of error messages. contact_url The contact URL for the package development. This information is only employed to prepare the text of error messages. dependencies_expected_prefix The expected prefix of import locations managed by the system manager. This information is employed while determining the import location of each dependency and to prepare the text of error messages. dependencies_import_name The import name of the dependencies of the package. This information is employed while determining the import location of each dependency and to prepare the text of error messages. dependencies_pypi_name The pypi name of the dependencies of the package. This information is only employed to prepare the text of error messages. dependencies_optional A list of bools reporting whether each dependence is optional or mandatory. This information is employed while determining the import location of each dependency and to prepare the text of error messages. dependencies_extra_error_message Additional text, corresponding to each dependency, to be added in the error message. This information is only employed to prepare the text of error messages. pip_uninstall_call A function that, given the python exectuable, the pypi name of a dependency of the package, and the path it has actually been imported from, returns the string to be reported to the user on how to uninstall it with pip. Raises ------ ImportError If at least a dependency is imported from user-site, or if at least a mandatory dependency is broken or missing. """ assert len(dependencies_import_name) == len(dependencies_pypi_name), "Incorrect input lengths" assert len(dependencies_import_name) == len(dependencies_optional), "Incorrect input lengths" assert len(dependencies_import_name) == len(dependencies_extra_error_message), "Incorrect input lengths" allow_user_site_imports_env_name = f"{package_name}_allow_user_site_imports".upper() allow_user_site_imports_env_value = os.getenv(allow_user_site_imports_env_name) is not None if not allow_user_site_imports_env_value: missing_dependencies: typing.List[typing.Optional[str]] = [None] * len(dependencies_import_name) broken_dependencies: typing.List[typing.Optional[typing.Dict[str, str]]] = [ None] * len(dependencies_import_name) user_site_dependencies: typing.List[typing.Optional[typing.Dict[str, str]]] = [ None] * len(dependencies_import_name) for (dependency_id, dependency_import_name) in enumerate(dependencies_import_name): dependency_module_expected_path = f"{dependencies_expected_prefix}/{dependency_import_name}/__init__.py" if not os.path.exists(dependency_module_expected_path) and not dependencies_optional[dependency_id]: missing_dependencies[dependency_id] = dependency_module_expected_path else: try: dependency_module = importlib.import_module(dependency_import_name) except BaseException as dependency_module_import_error: if not dependencies_optional[dependency_id]: broken_dependencies[dependency_id] = { "expected": dependency_module_expected_path, "error": str(dependency_module_import_error) } else: if dependency_module.__file__ != dependency_module_expected_path: assert dependency_module.__file__ is not None, f"Unable to find location of {dependency_module}" user_site_dependencies[dependency_id] = { "expected": dependency_module_expected_path, "actual": dependency_module.__file__ } counter_error_categories = 1 missing_dependencies_error = "" missing_dependencies_fix = "" if any([isinstance(dependency_expected_path, str) for dependency_expected_path in missing_dependencies]): missing_dependencies_error += f"{counter_error_categories}) Missing dependencies:\n" for (dependency_id, dependency_expected_path) in enumerate(missing_dependencies): if isinstance(dependency_expected_path, str): missing_dependencies_error += ( f"* {dependencies_import_name[dependency_id]} is missing. " f"Its expected path was {dependency_expected_path}.\n" ) missing_dependencies_fix += f"{counter_error_categories}) To install missing dependencies:\n" for (dependency_id, dependency_expected_path) in enumerate(missing_dependencies): if isinstance(dependency_expected_path, str): missing_dependencies_fix += ( f"* check how to install {dependencies_import_name[dependency_id]} " f"with {system_manager}.\n" ) counter_error_categories += 1 broken_dependencies_error = "" broken_dependencies_fix = "" if any([isinstance(dependency_info, dict) for dependency_info in broken_dependencies]): broken_dependencies_error += f"{counter_error_categories}) Broken dependencies:\n" for (dependency_id, dependency_info) in enumerate(broken_dependencies): if isinstance(dependency_info, dict): broken_dependencies_error += ( f"* {dependencies_import_name[dependency_id]} is broken. " f"Error on import was '{dependency_info['error']}'.\n" ) broken_dependencies_fix += f"{counter_error_categories}) To fix broken dependencies:\n" for (dependency_id, dependency_info) in enumerate(broken_dependencies): if isinstance(dependency_info, dict): broken_dependencies_fix += ( f"* run '{sys.executable} -m pip show {dependencies_pypi_name[dependency_id]}' in a terminal: " f"if the location field is not {os.path.dirname(os.path.dirname(dependency_info['expected']))} " f"consider running " f"'{pip_uninstall_call(sys.executable, dependencies_pypi_name[dependency_id], 'unknown')}' " "in a terminal, because the broken dependency is probably being imported from a local path " f"rather than from the path provided by {system_manager}. " f"{dependencies_extra_error_message[dependency_id]}\n" ) counter_error_categories += 1 user_site_dependencies_error = "" user_site_dependencies_fix = "" if any([isinstance(dependency_info, dict) for dependency_info in user_site_dependencies]): user_site_dependencies_error += ( f"{counter_error_categories}) Dependencies imported from a local path rather than from " f"the path provided by {system_manager}:\n" ) for (dependency_id, dependency_info) in enumerate(user_site_dependencies): if isinstance(dependency_info, dict): user_site_dependencies_error += ( f"* {dependencies_import_name[dependency_id]} was imported from a local path: " f"expected in {dependency_info['expected']}, but imported from {dependency_info['actual']}.\n" ) user_site_dependencies_fix += f"{counter_error_categories}) To uninstall local dependencies:\n" for (dependency_id, dependency_info) in enumerate(user_site_dependencies): if isinstance(dependency_info, dict): user_site_dependencies_fix += ( "* run " f"""'{pip_uninstall_call( sys.executable, dependencies_pypi_name[dependency_id], dependency_info['actual'])}' """ "in a terminal, and verify that you are prompted to confirm removal of files in " f"{os.path.dirname(dependency_info['actual'])}. " f"{dependencies_extra_error_message[dependency_id]}\n" ) counter_error_categories += 1 if counter_error_categories > 1: import_error = ( f"pusimp has detected the following problems with {package_name} dependencies:\n" f"{missing_dependencies_error}" f"{broken_dependencies_error}" f"{user_site_dependencies_error}" "\n" "pusimp suggests to apply all of the following fixes:\n" f"{missing_dependencies_fix}" f"{broken_dependencies_fix}" f"{user_site_dependencies_fix}" "\n" f"You can disable this check by exporting the {allow_user_site_imports_env_name} environment " f"variable. Note, however, that this may break the installation provided by {system_manager}.\n" f"If you believe that this message appears incorrectly, report this at {contact_url} ." ) raise ImportError(import_error) pusimp-0.1.1/pusimp/py.typed000066400000000000000000000000001463551352100160350ustar00rootroot00000000000000pusimp-0.1.1/pusimp/utils.py000066400000000000000000000361461463551352100160740ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Utility functions used while testing the package. Note that this file does not get automatically imported in __init__.py to avoid having a runtime dependency on virtualenv. """ import os import pathlib import shutil import subprocess import sys import tempfile import types import typing import virtualenv def assert_has_package(executable: str, package: str) -> None: """Assert that a package is installed. Note that it is not safe to simply import the package in the current pytest environment, since the environment itself might change from one test to the other, but python packages can be imported only once and not unloaded. """ run_import = subprocess.run(f"{executable} -c 'import {package}'", shell=True, capture_output=True) assert run_import.returncode == 0, ( f"Importing {package} was not successful.\n" f"stdout contains {run_import.stdout.decode().strip()}\n" f"stderr contains {run_import.stderr.decode().strip()}" ) def assert_not_has_package(executable: str, package: str) -> None: """Assert that a package is not installed.""" run_import = subprocess.run(f"{executable} -c 'import {package}'", shell=True, capture_output=True) assert run_import.returncode != 0, f"Importing {package} was unexpectedly successful" def assert_package_location(executable: str, package: str, package_path: str) -> None: """Assert that a package imports from the expected location.""" assert_has_package(executable, package) run_import_file = subprocess.run( f"{executable} -c 'import {package}; print({package}.__file__)'", shell=True, capture_output=True) assert run_import_file.returncode == 0, ( "This case was never supposed to happen, because {package} did import successfully with assert_has_package") assert run_import_file.stdout.decode().strip() == package_path, ( f"{package} was expected at {package_path}, but found at {run_import_file.stdout.decode().strip()}") def assert_package_import_error( executable: str, package: str, expected: typing.List[str], not_expected: typing.List[str], verbose: bool ) -> None: """Assert that a package fails to imports with the expected text in the ImportError message.""" run_import = subprocess.run(f"{executable} -c 'import {package}'", shell=True, capture_output=True) assert run_import.returncode != 0, f"Importing {package} was unexpectedly successful" import_error_text = ( f"Importing {package} was not successful.\n" f"stdout contains {run_import.stdout.decode().strip()}\n" f"stderr contains {run_import.stderr.decode().strip()}") if verbose: print(f"Package {package} did fail to import with error:\n{import_error_text}") for expected_ in expected: assert expected_ in import_error_text, ( f"'{expected_}' was not found in the ImportError text, namely '{import_error_text}'" ) for not_expected_ in not_expected: assert not_expected_ not in import_error_text, ( f"'{not_expected_}' was unexpectedly found in the ImportError text, namely '{import_error_text}'" ) class TemporarilyEnableEnvironmentVariable: """Temporarily enable an environment variable in a test.""" def __init__(self, variable_name: str) -> None: self._variable_name = variable_name def __enter__(self) -> None: """Temporarily set the environment variable.""" assert self._variable_name not in os.environ, f"{self._variable_name} was already found in the environment" os.environ[self._variable_name] = "enabled" def __exit__( self, exception_type: typing.Optional[typing.Type[BaseException]], exception_value: typing.Optional[BaseException], traceback: typing.Optional[types.TracebackType] ) -> None: """Unset the enviornment variable.""" del os.environ[self._variable_name] class VirtualEnv: """Helper class to create a temporary virtual environment. Forked and simplified from https://github.com/pyscaffold/pyscaffold/blob/master/tests/virtualenv.py . """ def __init__(self) -> None: self.path = pathlib.Path(tempfile.mkdtemp()) / "venv" self.dist_path = ( self.path / "lib" / ("python" + str(sys.version_info.major) + "." + str(sys.version_info.minor)) / "site-packages" ) self.executable = str(self.path / "bin" / "python3") self.env = dict(os.environ) self.env.pop("PYTHONPATH", None) # ensure isolation def __enter__(self) -> "VirtualEnv": """Create the virtual environment.""" assert not self.path.exists(), f"{self.path} already exists" self.create() return self def __exit__( self, exception_type: typing.Optional[typing.Type[BaseException]], exception_value: typing.Optional[BaseException], traceback: typing.Optional[types.TracebackType] ) -> None: """Delete the virtual environment.""" shutil.rmtree(str(self.path.parent), ignore_errors=True) def create(self) -> None: """Create a virtual environment, and add it to sys.path.""" args = [str(self.path), "--python", sys.executable, "--system-site-packages", "--no-wheel"] virtualenv.cli_run(args, env=self.env) # virtualenv does not necessarily ship the same version of pip as the underlying environment run_update_pip = subprocess.run( f"{self.executable} -m pip install --upgrade --break-system-packages pip", shell=True, env=self.env, capture_output=True) if run_update_pip.returncode != 0: # pragma: no cover # it is possible that the version of pip shipped by virtualenv was not recent enough # to support --break-system-packages. The newly installed version will surely support # --break-system-packages, so we can always add that flag in self.install_package run_update_pip_again = subprocess.run( f"{self.executable} -m pip install --upgrade pip", shell=True, capture_output=True) assert run_update_pip_again.returncode == 0, "Failed to upgrade pip" def install_package( self, package: str, install_call: typing.Optional[typing.Callable[[str, str], str]] = None ) -> None: """Install a package in the virtual environment.""" if install_call is None: install_call = self._default_install_call run_install = subprocess.run( install_call(self.executable, package), shell=True, env=self.env, capture_output=True) if run_install.returncode != 0: raise RuntimeError( f"Installing {package} was not successful.\n" f"stdout contains {run_install.stdout.decode()}\n" f"stderr contains {run_install.stderr.decode()}" ) @staticmethod def _default_install_call(executable: str, package: str) -> str: """Return the default call to pip install.""" return f"{executable} -m pip install --ignore-installed --break-system-packages {package}" def break_package(self, package: str) -> None: """Install a mock package in the virtual environment which errors out.""" (self.dist_path / package).mkdir() with (self.dist_path / package / "__init__.py").open("w") as init_file: init_file.write(f"raise ImportError('{package} was purposely broken.')") def uninstall_package( self, package: str, installation_path: str, uninstall_call: typing.Optional[typing.Callable[[str, str, str], str]] = None ) -> None: """Uninstall a package from the virtual environment.""" if uninstall_call is None: uninstall_call = self._default_uninstall_call run_uninstall = subprocess.run( uninstall_call(self.executable, package, installation_path), shell=True, env=self.env, capture_output=True) if ( run_uninstall.returncode != 0 or ( run_uninstall.returncode == 0 and f"WARNING: Skipping {package} as it is not installed" in run_uninstall.stderr.decode() ) ): raise RuntimeError( f"Uninstalling {package} was not successful.\n" f"stdout contains {run_uninstall.stdout.decode()}\n" f"stderr contains {run_uninstall.stderr.decode()}" ) @staticmethod def _default_uninstall_call(executable: str, package: str, installation_path: str) -> str: """Return the default call to pip uninstall.""" return f"{executable} -m pip uninstall --yes --break-system-packages {package}" def assert_package_import_success_without_local_packages(package: str, package_path: str) -> None: """Assert that the package imports correctly without any local packages.""" assert_package_location(sys.executable, package, package_path) def assert_package_import_errors_with_local_packages( package: str, dependencies_import_name: typing.List[str], dependencies_pypi_name: typing.List[str], dependencies_extra_error_message: typing.List[str], pip_install_call: typing.Callable[[str, str], str], pip_uninstall_call: typing.Callable[[str, str, str], str] ) -> None: """Assert that a package fails to import with local packages, but imports successfully when they are uninstalled.""" with VirtualEnv() as virtual_env: # Part 1: assert that the package fails to import with local packages dependencies_local_paths = [] for (dependency_import_name, dependency_pypi_name) in zip(dependencies_import_name, dependencies_pypi_name): virtual_env.install_package(dependency_pypi_name, pip_install_call) dependency_local_path = str(virtual_env.dist_path / dependency_import_name / "__init__.py") assert_package_location(virtual_env.executable, dependency_import_name, dependency_local_path) dependencies_local_paths.append(dependency_local_path) dependencies_error_messages = [ f"* {dependency_import_name} was imported from a local path: expected in" for dependency_import_name in dependencies_import_name ] dependencies_pypi_name_only = [ dependency_pypi_name.replace("'", "").split("@")[0].strip() # from 'name @ git+url' to name for dependency_pypi_name in dependencies_pypi_name ] dependencies_error_messages.extend( f"* run '{pip_uninstall_call(virtual_env.executable, dependency_pypi_name, dependency_local_path)}' in" for (dependency_pypi_name, dependency_local_path) in zip( dependencies_pypi_name_only, dependencies_local_paths) ) dependencies_error_messages.extend(dependencies_extra_error_message) assert_package_import_error(virtual_env.executable, package, dependencies_error_messages, [], True) # Part 2: assert that the package imports successfully as soon as local packages are uninstalled for (dependency_pypi_name, dependency_local_path) in zip(dependencies_pypi_name_only, dependencies_local_paths): virtual_env.uninstall_package( dependency_pypi_name, dependency_local_path, _force_yes_in_pip_uninstall_call(pip_uninstall_call)) assert_has_package(virtual_env.executable, package) def _force_yes_in_pip_uninstall_call( pip_uninstall_call: typing.Callable[[str, str, str], str] ) -> typing.Callable[[str, str, str], str]: """Force pip uninstall --yes even when a plain pip uninstall is provided.""" def _(executable: str, package: str, installation_path: str) -> str: base_call = pip_uninstall_call(executable, package, installation_path) if " -y " not in base_call and " --yes " not in base_call: base_call = base_call.replace(" uninstall ", " uninstall -y ") return base_call return _ def assert_package_import_success_with_allowed_local_packages( package: str, package_path: str, dependencies_import_name: typing.List[str], dependencies_pypi_name: typing.List[str], pip_install_call: typing.Callable[[str, str], str] ) -> None: """Assert that a package imports correctly even with extra local packages when asked to allow user-site imports.""" with VirtualEnv() as virtual_env: for (dependency_import_name, dependency_pypi_name) in zip(dependencies_import_name, dependencies_pypi_name): virtual_env.install_package(dependency_pypi_name, pip_install_call) assert_package_location( virtual_env.executable, dependency_import_name, str(virtual_env.dist_path / dependency_import_name / "__init__.py") ) with TemporarilyEnableEnvironmentVariable(f"{package}_allow_user_site_imports".upper()): assert_package_location(virtual_env.executable, package, package_path) def assert_package_import_errors_with_broken_non_optional_packages( package: str, dependencies_import_name: typing.List[str], dependencies_optional: typing.List[bool] ) -> None: """Assert that a package fails to import when non-optional packages are broken.""" with VirtualEnv() as virtual_env: for (dependency_import_name, dependency_optional) in zip(dependencies_import_name, dependencies_optional): if not dependency_optional: virtual_env.break_package(dependency_import_name) assert_package_import_error( virtual_env.executable, dependency_import_name, [f"{dependency_import_name} was purposely broken."], [], False ) dependencies_expected_error_messages = [ f"* {dependency_import_name} is broken" for (dependency_import_name, dependency_optional) in zip(dependencies_import_name, dependencies_optional) if not dependency_optional ] dependencies_not_expected_error_messages = [ f"* {dependency_import_name} is broken" for (dependency_import_name, dependency_optional) in zip(dependencies_import_name, dependencies_optional) if dependency_optional ] assert_package_import_error( virtual_env.executable, package, dependencies_expected_error_messages, dependencies_not_expected_error_messages, True ) def assert_package_import_success_with_broken_optional_packages( package: str, package_path: str, dependencies_import_name: typing.List[str], dependencies_optional: typing.List[bool] ) -> None: """Assert that a package imports correctly when optional packages are broken.""" with VirtualEnv() as virtual_env: for (dependency_import_name, dependency_optional) in zip(dependencies_import_name, dependencies_optional): if dependency_optional: virtual_env.break_package(dependency_import_name) assert_package_import_error( virtual_env.executable, dependency_import_name, [f"{dependency_import_name} was purposely broken."], [], False ) assert_package_location(virtual_env.executable, package, package_path) pusimp-0.1.1/pyproject.toml000066400000000000000000000045111463551352100157500ustar00rootroot00000000000000[build-system] requires = ["setuptools>=62", "wheel"] build-backend = "setuptools.build_meta" [project] name = "pusimp" version = "0.1.1" authors = [ {name = "Francesco Ballarin", email = "francesco.ballarin@unicatt.it"}, {name = "Drew Parsons", email = "dparsons@debian.org"}, ] maintainers = [ {name = "Francesco Ballarin", email = "francesco.ballarin@unicatt.it"}, {name = "Drew Parsons", email = "dparsons@debian.org"}, ] description = "Prevent user-site imports" readme = "README.md" license = {file = "LICENSE"} requires-python = ">=3.8" classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [] [project.urls] repository = "https://github.com/python-pusimp/pusimp" issues = "https://github.com/python-pusimp/pusimp/issues" [project.optional-dependencies] lint = [ "isort", "mypy", "ruff", "yamllint" ] tests = [ "coverage[toml]", "pip", "pytest", "virtualenv" ] [tool.isort] line_length = 120 multi_line_output = 4 order_by_type = false [tool.mypy] check_untyped_defs = true disallow_any_unimported = true disallow_untyped_defs = true no_implicit_optional = true pretty = true show_error_codes = true strict = true warn_return_any = true warn_unused_ignores = true [[tool.mypy.overrides]] module = [ "pusimp_dependency_missing", "tomllib", "virtualenv" ] ignore_missing_imports = true [tool.pytest.ini_options] [tool.ruff] line-length = 120 [tool.ruff.lint] select = ["ANN", "D", "E", "F", "FLY", "ICN", "N", "Q", "RUF", "UP", "W"] ignore = ["ANN101"] [tool.ruff.lint.pycodestyle] max-doc-length = 120 [tool.ruff.lint.pydocstyle] convention = "numpy" [tool.setuptools.package-data] pusimp = ["py.typed"] [tool.setuptools.packages.find] namespaces = false pusimp-0.1.1/tests/000077500000000000000000000000001463551352100141755ustar00rootroot00000000000000pusimp-0.1.1/tests/data/000077500000000000000000000000001463551352100151065ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_five/000077500000000000000000000000001463551352100216325ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_five/pusimp_dependency_five/000077500000000000000000000000001463551352100263565ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_five/pusimp_dependency_five/__init__.py000066400000000000000000000003521463551352100304670ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock dependency for pusimp tests. This dependency will always be broken by a user-site install in pusimp tests. """ pusimp-0.1.1/tests/data/pusimp_dependency_five/pyproject.toml000066400000000000000000000000741463551352100245470ustar00rootroot00000000000000[project] name = "pusimp-dependency-five" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_dependency_four/000077500000000000000000000000001463551352100216545ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_four/pusimp_dependency_four/000077500000000000000000000000001463551352100264225ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_four/pusimp_dependency_four/__init__.py000066400000000000000000000004601463551352100305330ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock dependency for pusimp tests. This dependency raises an error on import to mimick the case of a broken package. """ raise RuntimeError("pusimp_dependency_four is a broken package.") pusimp-0.1.1/tests/data/pusimp_dependency_four/pyproject.toml000066400000000000000000000000741463551352100245710ustar00rootroot00000000000000[project] name = "pusimp-dependency-four" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_dependency_one/000077500000000000000000000000001463551352100214625ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_one/pusimp_dependency_one/000077500000000000000000000000001463551352100260365ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_one/pusimp_dependency_one/__init__.py000066400000000000000000000003511463551352100301460ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock dependency for pusimp tests. This dependency will never be broken by a user-site install in pusimp tests. """ pusimp-0.1.1/tests/data/pusimp_dependency_one/pyproject.toml000066400000000000000000000000731463551352100243760ustar00rootroot00000000000000[project] name = "pusimp-dependency-one" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_dependency_six/000077500000000000000000000000001463551352100215045ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_six/pusimp_dependency_six/000077500000000000000000000000001463551352100261025ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_six/pusimp_dependency_six/__init__.py000066400000000000000000000004131463551352100302110ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock dependency for pusimp tests. This dependency will always be broken by a user-site install in pusimp tests, but will be considered optional. """ pusimp-0.1.1/tests/data/pusimp_dependency_six/pyproject.toml000066400000000000000000000000731463551352100244200ustar00rootroot00000000000000[project] name = "pusimp-dependency-six" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_dependency_three/000077500000000000000000000000001463551352100220105ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_three/pusimp_dependency_three/000077500000000000000000000000001463551352100267125ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_three/pusimp_dependency_three/__init__.py000066400000000000000000000004161463551352100310240ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock dependency for pusimp tests. This dependency will sometimes be broken by a user-site install in pusimp tests, but will be considered optional. """ pusimp-0.1.1/tests/data/pusimp_dependency_three/pyproject.toml000066400000000000000000000000751463551352100247260ustar00rootroot00000000000000[project] name = "pusimp-dependency-three" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_dependency_two/000077500000000000000000000000001463551352100215125ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_two/pusimp_dependency_two/000077500000000000000000000000001463551352100261165ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_dependency_two/pusimp_dependency_two/__init__.py000066400000000000000000000003551463551352100302320ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock dependency for pusimp tests. This dependency will sometimes be broken by a user-site install in pusimp tests. """ pusimp-0.1.1/tests/data/pusimp_dependency_two/pyproject.toml000066400000000000000000000000731463551352100244260ustar00rootroot00000000000000[project] name = "pusimp-dependency-two" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_golden_source/000077500000000000000000000000001463551352100213335ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_golden_source/pusimp_golden_source/000077500000000000000000000000001463551352100255605ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_golden_source/pusimp_golden_source/__init__.py000066400000000000000000000012251463551352100276710ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Golden source for pusimp. The goal of this package is to provide a mock system path, a mock contact URL, and a mock system package manager. """ import os contact_url = "mock contact URL" system_path = os.path.dirname(os.path.dirname(__file__)) system_package_manager = "mock system package manager" def pip_uninstall_call(executable: str, dependency_pypi_name: str, dependency_actual_path: str) -> str: """Report to the user how to uninstall a dependency with pip.""" return f"{executable} -m pip uninstall {dependency_pypi_name}" pusimp-0.1.1/tests/data/pusimp_golden_source/pyproject.toml000066400000000000000000000000721463551352100242460ustar00rootroot00000000000000[project] name = "pusimp-golden-source" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_eight/000077500000000000000000000000001463551352100212565ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_eight/pusimp_package_eight/000077500000000000000000000000001463551352100254265ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_eight/pusimp_package_eight/__init__.py000066400000000000000000000023211463551352100275350ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This package imports pusimp_dependency_five and pusimp_dependency_six. pusimp_dependency_five is a mandatory dependency, while pusimp_dependency_six is an optional dependency. Both dependencies will always be marked as imported from a user site. The main difference compared to pusimp_package_two and pusimp_package_three is that their dependencies there will sometimes be imported from a user site, while here the dependencies are always imported from a user site. """ import pusimp_dependency_five # noqa: F401 import pusimp_golden_source try: import pusimp_dependency_six # noqa: F401 except ImportError: pass import pusimp pusimp.prevent_user_site_imports( "pusimp_package_eight", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_five", "pusimp_dependency_six"], ["pusimp-dependency-five", "pusimp-dependency-six"], [False, True], ["pusimp_dependency_five is mandatory.", "pusimp_dependency_six is optional."], pusimp_golden_source.pip_uninstall_call ) pusimp-0.1.1/tests/data/pusimp_package_eight/pyproject.toml000066400000000000000000000000721463551352100241710ustar00rootroot00000000000000[project] name = "pusimp-package-eight" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_five/000077500000000000000000000000001463551352100211075ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_five/pusimp_package_five/000077500000000000000000000000001463551352100251105ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_five/pusimp_package_five/__init__.py000066400000000000000000000016511463551352100272240ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This is the same as pusimp_package_four, except for the fact that pusimp_dependency_missing is now a optional dependency. Therefore, even though it will always be missing, this package will not raise an ImportError on import. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_five", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_missing"], ["pusimp-dependency-one", "pusimp-dependency-missing"], [False, True], ["", ""], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_one # noqa: E402, F401 try: import pusimp_dependency_missing # noqa: F401 except ImportError: pass pusimp-0.1.1/tests/data/pusimp_package_five/pyproject.toml000066400000000000000000000000711463551352100240210ustar00rootroot00000000000000[project] name = "pusimp-package-five" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_four/000077500000000000000000000000001463551352100211315ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_four/pusimp_package_four/000077500000000000000000000000001463551352100251545ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_four/pusimp_package_four/__init__.py000066400000000000000000000016011463551352100272630ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This package imports pusimp_dependency_one and pusimp_dependency_missing. pusimp_dependency_missing is a mandatory dependency and will always be missing, and therefore this package will raise an ImportError on import. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_four", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_missing"], ["pusimp-dependency-one", "pusimp-dependency-missing"], [False, False], ["", ""], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_missing # noqa: E402, F401 import pusimp_dependency_one # noqa: E402, F401 pusimp-0.1.1/tests/data/pusimp_package_four/pyproject.toml000066400000000000000000000000711463551352100240430ustar00rootroot00000000000000[project] name = "pusimp-package-four" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_nine/000077500000000000000000000000001463551352100211075ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_nine/pusimp_package_nine/000077500000000000000000000000001463551352100251105ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_nine/pusimp_package_nine/__init__.py000066400000000000000000000016721463551352100272270ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This is the same as pusimp_package_eight, except for the fact that pusimp.prevent_user_site_imports is called before the imports of pusimp_dependency_five and pusimp_dependency_six. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_nine", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_five", "pusimp_dependency_six"], ["pusimp-dependency-five", "pusimp-dependency-six"], [False, True], ["pusimp_dependency_five is mandatory.", "pusimp_dependency_six is optional."], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_five # noqa: E402, F401 try: import pusimp_dependency_six # noqa: F401 except ImportError: pass pusimp-0.1.1/tests/data/pusimp_package_nine/pyproject.toml000066400000000000000000000000711463551352100240210ustar00rootroot00000000000000[project] name = "pusimp-package-nine" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_one/000077500000000000000000000000001463551352100207375ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_one/pusimp_package_one/000077500000000000000000000000001463551352100245705ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_one/pusimp_package_one/__init__.py000066400000000000000000000014731463551352100267060ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This package imports pusimp_dependency_one and pusimp_dependency_two. pusimp_dependency_two will sometimes be broken by user-site imports. """ import pusimp_dependency_one # noqa: F401 import pusimp_dependency_two # noqa: F401 import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_one", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_two"], ["pusimp-dependency-one", "pusimp-dependency-two"], [False, False], ["", "pusimp_dependency_two is mandatory."], pusimp_golden_source.pip_uninstall_call ) pusimp-0.1.1/tests/data/pusimp_package_one/pyproject.toml000066400000000000000000000000701463551352100236500ustar00rootroot00000000000000[project] name = "pusimp-package-one" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_seven/000077500000000000000000000000001463551352100212765ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_seven/pusimp_package_seven/000077500000000000000000000000001463551352100254665ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_seven/pusimp_package_seven/__init__.py000066400000000000000000000016511463551352100276020ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This is the same as pusimp_package_six, except for the fact that pusimp_dependency_four is now a optional dependency. Therefore, even though it will always be raising an error, this package will not raise an ImportError on import. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_seven", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_four"], ["pusimp-dependency-one", "pusimp-dependency-four"], [False, True], ["", ""], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_one # noqa: E402, F401 try: import pusimp_dependency_missing # noqa: F401 except ImportError: pass pusimp-0.1.1/tests/data/pusimp_package_seven/pyproject.toml000066400000000000000000000000721463551352100242110ustar00rootroot00000000000000[project] name = "pusimp-package-seven" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_six/000077500000000000000000000000001463551352100207615ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_six/pusimp_package_six/000077500000000000000000000000001463551352100246345ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_six/pusimp_package_six/__init__.py000066400000000000000000000015651463551352100267540ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This package imports pusimp_dependency_one and pusimp_dependency_four. pusimp_dependency_four is a mandatory dependency and will always raise an error, and therefore this package will raise an ImportError on import. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_six", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_four"], ["pusimp-dependency-one", "pusimp-dependency-four"], [False, False], ["", ""], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_four # noqa: E402, F401 import pusimp_dependency_one # noqa: E402, F401 pusimp-0.1.1/tests/data/pusimp_package_six/pyproject.toml000066400000000000000000000000701463551352100236720ustar00rootroot00000000000000[project] name = "pusimp-package-six" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_ten/000077500000000000000000000000001463551352100207445ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_ten/pusimp_package_ten/000077500000000000000000000000001463551352100246025ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_ten/pusimp_package_ten/__init__.py000066400000000000000000000026031463551352100267140ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This package imports: * pusimp_dependency_four, a mandatory dependency which will always be broken, * pusimp_dependency_five, a mandatory dependency, and which will always be marked as imported from a user site, * pusimp_dependency_six, an optional dependency, and which will always be marked as imported from a user site, * pusimp_dependency_missing, a mandatory dependency which will always be missing. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_ten", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_four", "pusimp_dependency_five", "pusimp_dependency_six", "pusimp_dependency_missing"], ["pusimp-dependency-four", "pusimp-dependency-five", "pusimp-dependency-six", "pusimp-dependency-missing"], [False, False, True, False], ["", "pusimp_dependency_five is mandatory.", "pusimp_dependency_six is optional.", ""], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_five # noqa: E402, F401 import pusimp_dependency_four # noqa: E402, F401 import pusimp_dependency_missing # noqa: E402, F401 try: import pusimp_dependency_six # noqa: F401 except ImportError: pass pusimp-0.1.1/tests/data/pusimp_package_ten/pyproject.toml000066400000000000000000000000701463551352100236550ustar00rootroot00000000000000[project] name = "pusimp-package-ten" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_three/000077500000000000000000000000001463551352100212655ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_three/pusimp_package_three/000077500000000000000000000000001463551352100254445ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_three/pusimp_package_three/__init__.py000066400000000000000000000021031463551352100275510ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This is the same as pusimp_package_two, except for the fact that pusimp.prevent_user_site_imports is called before the imports of pusimp_dependency_one, pusimp_dependency_two and pusimp_dependency_three. """ import pusimp_golden_source import pusimp pusimp.prevent_user_site_imports( "pusimp_package_three", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_two", "pusimp_dependency_three"], ["pusimp-dependency-one", "pusimp-dependency-two", "pusimp-dependency-three"], [False, False, True], ["", "pusimp_dependency_two is mandatory.", "pusimp_dependency_three is optional."], pusimp_golden_source.pip_uninstall_call ) import pusimp_dependency_one # noqa: E402, F401 import pusimp_dependency_two # noqa: E402, F401 try: import pusimp_dependency_three # noqa: F401 except ImportError: pass pusimp-0.1.1/tests/data/pusimp_package_three/pyproject.toml000066400000000000000000000000721463551352100242000ustar00rootroot00000000000000[project] name = "pusimp-package-three" version = "0.1.1" pusimp-0.1.1/tests/data/pusimp_package_two/000077500000000000000000000000001463551352100207675ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_two/pusimp_package_two/000077500000000000000000000000001463551352100246505ustar00rootroot00000000000000pusimp-0.1.1/tests/data/pusimp_package_two/pusimp_package_two/__init__.py000066400000000000000000000022211463551352100267560ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Mock package for pusimp tests. This package imports pusimp_dependency_one, pusimp_dependency_two and pusimp_dependency_three. pusimp_dependency_two will sometimes be broken by user-site imports, and is a mandatory dependency. pusimp_dependency_three will sometimes be broken by user-site imports, and is an optional dependency. """ import pusimp_dependency_one # noqa: F401 import pusimp_dependency_two # noqa: F401 import pusimp_golden_source try: import pusimp_dependency_three # noqa: F401 except ImportError: pass import pusimp pusimp.prevent_user_site_imports( "pusimp_package_two", pusimp_golden_source.system_package_manager, pusimp_golden_source.contact_url, pusimp_golden_source.system_path, ["pusimp_dependency_one", "pusimp_dependency_two", "pusimp_dependency_three"], ["pusimp-dependency-one", "pusimp-dependency-two", "pusimp-dependency-three"], [False, False, True], ["", "pusimp_dependency_two is mandatory.", "pusimp_dependency_three is optional."], pusimp_golden_source.pip_uninstall_call ) pusimp-0.1.1/tests/data/pusimp_package_two/pyproject.toml000066400000000000000000000000701463551352100237000ustar00rootroot00000000000000[project] name = "pusimp-package-two" version = "0.1.1" pusimp-0.1.1/tests/unit/000077500000000000000000000000001463551352100151545ustar00rootroot00000000000000pusimp-0.1.1/tests/unit/test_data.py000066400000000000000000000231401463551352100174760ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Test imports of mock packages in tests/data.""" import importlib import os import shutil import sys import tempfile import pytest import pusimp_golden_source # isort: skip all_mock_packages = [ "pusimp_package_one", "pusimp_package_two", "pusimp_package_three", "pusimp_package_four", "pusimp_package_five", "pusimp_package_six", "pusimp_package_seven", "pusimp_package_eight", "pusimp_package_nine" ] all_mock_dependencies = [ "pusimp_dependency_one", "pusimp_dependency_two", "pusimp_dependency_three", "pusimp_dependency_four", "pusimp_dependency_five", "pusimp_dependency_six" ] all_mock_golden = [ "pusimp_golden_source" ] def test_data_versions() -> None: """Test that the version of every mock package, dependency and golden is the same as the main package.""" pusimp_version = importlib.metadata.version("pusimp") if "9999" in pusimp_version: # Versions on TestPyPI have a mock version number formed by the actual version number, a separator 9999, # and then the upload date to work around the fact that releases cannot be overwritten. Recognize this # case and transform the mock version number into the actual version number. pusimp_version, _ = pusimp_version.split("9999") if pusimp_version[-1] == ".": # Leading zeros are not conserved when preparing the distribution: just add it back. pusimp_version = f"{pusimp_version}0" for mock in all_mock_packages + all_mock_dependencies + all_mock_golden: assert importlib.metadata.version(mock.replace("_", "-")) == pusimp_version def test_data_one() -> None: """Test that the first mock package in tests/data import correctly.""" import pusimp_package_one # noqa: F401 def test_data_two() -> None: """Test that the second mock package in tests/data import correctly.""" import pusimp_package_two # noqa: F401 def test_data_three() -> None: """Test that the third mock package in tests/data import correctly.""" import pusimp_package_three # noqa: F401 def test_data_four() -> None: """Test that the fourth mock package in tests/data fails to import due to a missing mandatory dependency.""" with pytest.raises(ImportError) as excinfo: import pusimp_package_four # noqa: F401 import_error_text = str(excinfo.value) print(f"The following ImportError was raised:\n{import_error_text}") assert "pusimp has detected the following problems with pusimp_package_four dependencies" in import_error_text assert "Missing dependencies:" in import_error_text assert ( "* pusimp_dependency_missing is missing. Its expected path was " f"{os.path.join(pusimp_golden_source.system_path, 'pusimp_dependency_missing', '__init__.py')}." ) in import_error_text assert "To install missing dependencies:" in import_error_text assert "* check how to install pusimp_dependency_missing with mock system package manager" in import_error_text assert "believe that this message appears incorrectly, report this at mock contact URL ." in import_error_text def test_data_five() -> None: """Test that the fifth mock package in tests/data import correctly, because the missing dependency is optional.""" import pusimp_package_five # noqa: F401 def test_data_six() -> None: """Test that the sixth mock package in tests/data fails to import due to a broken mandatory dependency.""" with pytest.raises(ImportError) as excinfo: import pusimp_package_six # noqa: F401 import_error_text = str(excinfo.value) print(f"The following ImportError was raised:\n{import_error_text}") assert "pusimp has detected the following problems with pusimp_package_six dependencies" in import_error_text assert "Broken dependencies:" in import_error_text assert ( "pusimp_dependency_four is broken. Error on import was 'pusimp_dependency_four is a broken package.'." ) in import_error_text assert "To fix broken dependencies:" in import_error_text assert ( f"* run '{sys.executable} -m pip show pusimp-dependency-four' in a terminal: if the location field is not " f"{pusimp_golden_source.system_path} consider running '{sys.executable} -m pip uninstall " "pusimp-dependency-four' in a terminal, because the broken dependency is probably being imported from " "a local path rather than from the path provided by mock system package manager." ) in import_error_text assert "believe that this message appears incorrectly, report this at mock contact URL ." in import_error_text def test_data_seven() -> None: """Test that the seventh mock package in tests/data import correctly, because the broken dependency is optional.""" import pusimp_package_seven # noqa: F401 def test_data_eight_nine_ten() -> None: """Test that the eighth to tenth mock package in tests/data fails to import due to dependencies from user site. Note that the eighth and ninth mock package are tested in the same function, rather than two separate functions, because they both the depend on pusimp_dependency_five and pusimp_dependency_six, which will get replaced by an user-site installation inside this test. However, since the python interpreter only loads a module once, we have to be sure that every mock package that requires pusimp_dependency_five and pusimp_dependency_six operates in the same environment. """ mock_system_site_path = pusimp_golden_source.system_path mock_user_site_path = tempfile.mkdtemp() sys.path.insert(0, mock_user_site_path) for dependency_import_name in ("pusimp_dependency_five", "pusimp_dependency_six"): dependency_user_site = os.path.join(mock_user_site_path, dependency_import_name) os.makedirs(dependency_user_site) with open(os.path.join(dependency_user_site, "__init__.py"), "w") as init_file: init_file.write("# created by pytest") try: for pusimp_package in ("pusimp_package_eight", "pusimp_package_nine", "pusimp_package_ten"): with pytest.raises(ImportError) as excinfo: importlib.import_module(pusimp_package) import_error_text = str(excinfo.value) print(f"The following ImportError was raised:\n{import_error_text}") assert ( f"pusimp has detected the following problems with {pusimp_package} dependencies" ) in import_error_text if pusimp_package == "pusimp_package_ten": assert "Missing dependencies:" in import_error_text assert "To install missing dependencies:" in import_error_text assert "Broken dependencies:" in import_error_text assert "To fix broken dependencies:" in import_error_text assert ( "Dependencies imported from a local path rather than from the path provided by " "mock system package manager:" ) in import_error_text assert "To uninstall local dependencies:" in import_error_text if pusimp_package == "pusimp_package_ten": assert ( "* pusimp_dependency_missing is missing. Its expected path was " f"{os.path.join(mock_system_site_path, 'pusimp_dependency_missing', '__init__.py')}." ) in import_error_text assert ( "* check how to install pusimp_dependency_missing with mock system package manager." ) in import_error_text assert ( "pusimp_dependency_four is broken. Error on import was " "'pusimp_dependency_four is a broken package.'." ) in import_error_text assert ( f"* run '{sys.executable} -m pip show pusimp-dependency-four' in a terminal: if the location " f"field is not {mock_system_site_path} consider running '{sys.executable} -m pip uninstall " "pusimp-dependency-four' in a terminal, because the broken dependency is probably being imported " "from a local path rather than from the path provided by mock system package manager." ) in import_error_text for (dependency_import_name, dependency_optional_string) in ( ("pusimp_dependency_five", "mandatory"), ("pusimp_dependency_six", "optional") ): assert ( f"* {dependency_import_name} was imported from a local path: expected in " f"{os.path.join(mock_system_site_path, dependency_import_name, '__init__.py')}, " f"but imported from " f"{os.path.join(mock_user_site_path, dependency_import_name, '__init__.py')}." ) in import_error_text dependency_pypi_name = dependency_import_name.replace("_", "-") assert ( f"* run '{sys.executable} -m pip uninstall {dependency_pypi_name}' in a terminal, " f"and verify that you are prompted to confirm removal of files in " f"{os.path.join(mock_user_site_path, dependency_import_name)}. " f"{dependency_import_name} is {dependency_optional_string}." ) in import_error_text assert ( "believe that this message appears incorrectly, report this at mock contact URL ." ) in import_error_text finally: del sys.path[0] shutil.rmtree(mock_user_site_path, ignore_errors=True) pusimp-0.1.1/tests/unit/test_readme.py000066400000000000000000000062401463551352100200240ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Test the snippet in README.md.""" import importlib import os import shutil import sys import tempfile import pytest def test_readme() -> None: """Test the snippet in README.md.""" # Get snippets from file readme = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "README.md") assert os.path.exists(readme) with open(readme) as readme_file: num_backticks_lines = 0 code_snippet = "" error_snippet = "" for line in readme_file.readlines(): if line.startswith("```"): num_backticks_lines += 1 else: if num_backticks_lines == 1: code_snippet += line elif num_backticks_lines == 3: error_snippet += line assert num_backticks_lines == 4 code_snippet = code_snippet.strip("\n") error_snippet = error_snippet.strip("\n") # Generate mock site paths readme_system_site_path = "/usr/lib/python3.xy/site-packages" readme_user_site_path = "~/.local/lib/python3.xy/site-packages" mock_executable_path = "python3" mock_system_site_path = tempfile.mkdtemp() mock_user_site_path = tempfile.mkdtemp() sys.path.insert(0, mock_user_site_path) sys.path.insert(1, mock_system_site_path) # Write package and dependencies to disk packages_code = { "my_package": code_snippet.replace(readme_system_site_path, mock_system_site_path), # "my_dependency_one": "", # purposely missing "my_dependency_two": "raise RuntimeError('purposely broken')", "my_dependency_three": "", "my_dependency_four": "" } for (package_import_name, package_code) in packages_code.items(): for mock_site_path in (mock_user_site_path, mock_system_site_path): package_user_site = os.path.join(mock_site_path, package_import_name) os.makedirs(package_user_site) with open(os.path.join(package_user_site, "__init__.py"), "w") as init_file: init_file.write(package_code) shutil.rmtree(os.path.join(mock_user_site_path, "my_dependency_one"), ignore_errors=True) try: with pytest.raises(ImportError) as excinfo: importlib.import_module("my_package") import_error_text = str(excinfo.value) import_error_text = import_error_text.replace(f"{sys.executable} -m", f"{mock_executable_path} -m") import_error_text = import_error_text.replace(mock_system_site_path, readme_system_site_path) import_error_text = import_error_text.replace(mock_user_site_path, readme_user_site_path) import_error_text = "\n".join([line.rstrip() for line in import_error_text.splitlines()]) print(f"The following ImportError was raised:\n{import_error_text}") finally: del sys.path[0] del sys.path[1] shutil.rmtree(mock_user_site_path, ignore_errors=True) shutil.rmtree(mock_system_site_path, ignore_errors=True) # Check that the README is up to date with the current error message assert error_snippet == import_error_text pusimp-0.1.1/tests/unit/test_utils.py000066400000000000000000000612621463551352100177340ustar00rootroot00000000000000# Copyright (C) 2023-2024 by the pusimp authors # # This file is part of pusimp. # # SPDX-License-Identifier: MIT """Test utility functions defined in pusimp.utils.""" import os import sys import typing import pytest from pusimp.utils import ( assert_has_package, assert_not_has_package, assert_package_import_error, assert_package_import_errors_with_broken_non_optional_packages, assert_package_import_errors_with_local_packages, assert_package_import_success_with_allowed_local_packages, assert_package_import_success_with_broken_optional_packages, assert_package_import_success_without_local_packages, assert_package_location, VirtualEnv) import pusimp_golden_source # isort: skip def test_assert_has_package_success() -> None: """Test that assert_has_package succeeds with a package that is surely installed.""" assert_has_package(sys.executable, "pytest") def test_assert_has_package_failure() -> None: """Test that assert_has_package fails with a package that is surely not installed.""" with pytest.raises(AssertionError) as excinfo: assert_has_package(sys.executable, "not_existing_package") assertion_error_text = str(excinfo.value) assert assertion_error_text.startswith("Importing not_existing_package was not successful") def test_assert_not_has_package_success() -> None: """Test that assert_not_has_package succeeds with a package that is surely not installed.""" assert_not_has_package(sys.executable, "not_existing_package") def test_assert_not_has_package_failure() -> None: """Test that assert_not_has_package fails with a package that is surely installed.""" with pytest.raises(AssertionError) as excinfo: assert_not_has_package(sys.executable, "pytest") assertion_error_text = str(excinfo.value) assert assertion_error_text.startswith("Importing pytest was unexpectedly successful") def test_assert_package_location() -> None: """Test assert_package_location with a package that is surely installed.""" assert_package_location(sys.executable, "pytest", pytest.__file__) def test_assert_package_import_error() -> None: """Test assert_package_import_error with a package that is surely not installed.""" assert_package_import_error( sys.executable, "not_existing_package", ["No module named 'not_existing_package'"], [], False ) def test_virtual_env() -> None: """Test that the creation of a virtual environment is successful.""" with VirtualEnv() as virtual_env: assert virtual_env.path.exists() def test_install_package_in_virtual_env_success() -> None: """Test that installing a package in a virtual environment is successful.""" with VirtualEnv() as virtual_env: virtual_env.install_package("my-empty-package") assert_package_location( virtual_env.executable, "my_empty_package", str(virtual_env.dist_path / "my_empty_package" / "__init__.py") ) assert_package_import_error( sys.executable, "my_empty_package", ["No module named 'my_empty_package'"], [], False ) def test_install_package_in_virtual_env_custom_call_success() -> None: """Test that installing a package in a virtual environment with a custom installation call is successful.""" with VirtualEnv() as virtual_env: virtual_env.install_package( "my-empty-package", lambda executable, package: f"{executable} -m pip install {package}") assert_package_location( virtual_env.executable, "my_empty_package", str(virtual_env.dist_path / "my_empty_package" / "__init__.py") ) assert_package_import_error( sys.executable, "my_empty_package", ["No module named 'my_empty_package'"], [], False ) def test_install_package_in_virtual_env_failure() -> None: """Test that installing a package in a virtual environment is failing when the package does not exist on pypi.""" with VirtualEnv() as virtual_env: with pytest.raises(RuntimeError) as excinfo: virtual_env.install_package("not-existing-package") runtime_error_text = str(excinfo.value) assert runtime_error_text.startswith("Installing not-existing-package was not successful") def test_install_package_in_virtual_env_custom_call_failure() -> None: """Test that installing a package in a virtual environment is failing when passing a wrong custom call.""" with VirtualEnv() as virtual_env: with pytest.raises(RuntimeError) as excinfo: virtual_env.install_package( "not-really-used", lambda executable, package: f"{executable} -m pip --this-option-does-not-exist {package}") runtime_error_text = str(excinfo.value) assert runtime_error_text.startswith("Installing not-really-used was not successful") def test_break_package_in_virtual_env() -> None: """Test breaking an existing package by a mock installation in a virtual environment.""" with VirtualEnv() as virtual_env: virtual_env.break_package("pytest") assert_package_import_error(virtual_env.executable, "pytest", ["pytest was purposely broken."], [], False) assert_package_location(sys.executable, "pytest", pytest.__file__) def test_uninstall_package_in_virtual_env_success() -> None: """Test that uninstalling a package in a virtual environment is successful.""" with VirtualEnv() as virtual_env: virtual_env.install_package("my-empty-package") assert_package_location( virtual_env.executable, "my_empty_package", str(virtual_env.dist_path / "my_empty_package" / "__init__.py") ) virtual_env.uninstall_package("my-empty-package", "/not/really/used") assert_package_import_error( virtual_env.executable, "my_empty_package", ["No module named 'my_empty_package'"], [], False ) assert_package_import_error( sys.executable, "my_empty_package", ["No module named 'my_empty_package'"], [], False ) def test_uninstall_package_in_virtual_env_custom_call_success() -> None: """Test that uninstalling a package in a virtual environment with a custom uninstallation call is successful.""" with VirtualEnv() as virtual_env: virtual_env.install_package("my-empty-package") assert_package_location( virtual_env.executable, "my_empty_package", str(virtual_env.dist_path / "my_empty_package" / "__init__.py") ) virtual_env.uninstall_package( "my-empty-package", "/not/really/used", lambda executable, package, _: f"{executable} -m pip uninstall -y {package}") assert_package_import_error( virtual_env.executable, "my_empty_package", ["No module named 'my_empty_package'"], [], False ) assert_package_import_error( sys.executable, "my_empty_package", ["No module named 'my_empty_package'"], [], False ) def test_uninstall_package_in_virtual_env_failure() -> None: """Test that uninstalling a package in a virtual environment is failing when the package was never installed.""" with VirtualEnv() as virtual_env: with pytest.raises(RuntimeError) as excinfo: virtual_env.uninstall_package("not-installed-package", "/not/really/used") runtime_error_text = str(excinfo.value) assert runtime_error_text.startswith("Uninstalling not-installed-package was not successful") def test_uninstall_package_in_virtual_env_custom_call_failure() -> None: """Test that uninstalling a package in a virtual environment is failing when passing a wrong custom call.""" with VirtualEnv() as virtual_env: with pytest.raises(RuntimeError) as excinfo: virtual_env.uninstall_package( "not-really-used", "/not/really/used", lambda executable, package, _: f"{executable} -m pip uninstall --this-option-does-not-exist {package}") runtime_error_text = str(excinfo.value) assert runtime_error_text.startswith("Uninstalling not-really-used was not successful") def generate_test_data_pypi_names(import_names: typing.List[str]) -> typing.List[str]: """Replace underscore with dash in import names, and add installation from local directory.""" pypi_names = [import_name.replace("_", "-") for import_name in import_names] tests_data_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") return [ f"'{pypi_name} @ file://{tests_data_dir}/{import_name}'" for (import_name, pypi_name) in zip(import_names, pypi_names) ] @pytest.mark.parametrize( "package_name", [ "pusimp_package_one", "pusimp_package_two", "pusimp_package_three", # "pusimp_package_four", # raises ImportError due to mandatory missing dependency "pusimp_package_five", # "pusimp_package_six", # raises ImportError due to mandatory broken dependency "pusimp_package_seven", # "pusimp_package_eight", # its dependencies are supposed to always be local # "pusimp_package_nine", # its dependencies are supposed to always be local # "pusimp_package_ten", # its dependencies are supposed to either always local, missing or broken ] ) def test_assert_package_import_success_without_local_packages_data(package_name: str) -> None: """Test assert_package_import_success_without_local_packages on mock packages that don't raise errors on import.""" assert_package_import_success_without_local_packages( package_name, os.path.join(pusimp_golden_source.system_path, package_name, "__init__.py") ) @pytest.mark.parametrize( "package_name,dependencies_import_name,dependencies_extra_error_message", [ ("pusimp_package_one", ["pusimp_dependency_two"], ["pusimp_dependency_two is mandatory."]), ("pusimp_package_two", ["pusimp_dependency_two"], ["pusimp_dependency_two is mandatory."]), ("pusimp_package_two", ["pusimp_dependency_three"], ["pusimp_dependency_three is optional."]), ( "pusimp_package_two", ["pusimp_dependency_two", "pusimp_dependency_three"], ["pusimp_dependency_two is mandatory.", "pusimp_dependency_three is optional."] ), ("pusimp_package_three", ["pusimp_dependency_two"], ["pusimp_dependency_two is mandatory."]), ("pusimp_package_three", ["pusimp_dependency_three"], ["pusimp_dependency_three is optional."]), ( "pusimp_package_three", ["pusimp_dependency_two", "pusimp_dependency_three"], ["pusimp_dependency_two is mandatory.", "pusimp_dependency_three is optional."] ), # ("pusimp_package_four", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_five", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_six", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_seven", [], []), # pusimp_dependency_four is always broken ("pusimp_package_eight", ["pusimp_dependency_five"], ["pusimp_dependency_five is mandatory."]), ("pusimp_package_eight", ["pusimp_dependency_six"], ["pusimp_dependency_six is optional."]), ( "pusimp_package_eight", ["pusimp_dependency_five", "pusimp_dependency_six"], ["pusimp_dependency_five is mandatory.", "pusimp_dependency_six is optional."] ), ("pusimp_package_nine", ["pusimp_dependency_five"], ["pusimp_dependency_five is mandatory."]), ("pusimp_package_nine", ["pusimp_dependency_six"], ["pusimp_dependency_six is optional."]), ( "pusimp_package_nine", ["pusimp_dependency_five", "pusimp_dependency_six"], ["pusimp_dependency_five is mandatory.", "pusimp_dependency_six is optional."] ) # ( # "pusimp_package_ten", [], [] # ) # pusimp_dependency_missing is not installable, pusimp_dependency_four is always broken ] ) def test_assert_package_import_errors_with_local_packages_data( package_name: str, dependencies_import_name: typing.List[str], dependencies_extra_error_message: typing.List[str] ) -> None: """Test assert_package_import_errors_with_local_packages on mock packages.""" assert_package_import_errors_with_local_packages( package_name, dependencies_import_name, generate_test_data_pypi_names(dependencies_import_name), dependencies_extra_error_message, lambda executable, dependency_pypi_name: ( f"{executable} -m pip install --ignore-installed {dependency_pypi_name}"), lambda executable, dependency_pypi_name, _: f"{executable} -m pip uninstall {dependency_pypi_name}" ) @pytest.mark.parametrize( "package_name,dependencies_import_name", [ ("pusimp_package_one", ["pusimp_dependency_two"]), ("pusimp_package_two", ["pusimp_dependency_two"]), ("pusimp_package_two", ["pusimp_dependency_three"]), ("pusimp_package_two", ["pusimp_dependency_two", "pusimp_dependency_three"]), ("pusimp_package_three", ["pusimp_dependency_two"]), ("pusimp_package_three", ["pusimp_dependency_three"]), ("pusimp_package_three", ["pusimp_dependency_two", "pusimp_dependency_three"]), # ("pusimp_package_four", []), # pusimp_dependency_missing is not installable ("pusimp_package_five", []), # ("pusimp_package_six", []), # pusimp_dependency_four is always broken ("pusimp_package_seven", []), ("pusimp_package_eight", ["pusimp_dependency_five"]), ("pusimp_package_eight", ["pusimp_dependency_six"]), ("pusimp_package_eight", ["pusimp_dependency_five", "pusimp_dependency_six"]), ("pusimp_package_nine", ["pusimp_dependency_five"]), ("pusimp_package_nine", ["pusimp_dependency_six"]), ("pusimp_package_nine", ["pusimp_dependency_five", "pusimp_dependency_six"]) # ( # "pusimp_package_ten", [], [] # ), # pusimp_dependency_missing is not installable, pusimp_dependency_four is always broken ] ) def test_assert_package_import_success_with_allowed_local_packages_data( package_name: str, dependencies_import_name: typing.List[str] ) -> None: """Test assert_package_import_success_with_allowed_local_packages on mock packages.""" assert_package_import_success_with_allowed_local_packages( package_name, os.path.join(pusimp_golden_source.system_path, package_name, "__init__.py"), dependencies_import_name, generate_test_data_pypi_names(dependencies_import_name), lambda executable, dependency_pypi_name: ( f"{executable} -m pip install --ignore-installed {dependency_pypi_name}") ) @pytest.mark.parametrize( "package_name,dependencies_import_name,dependencies_optional", [ # ("pusimp_package_one", ["pusimp_dependency_two"], [False]), # in failure_position # ("pusimp_package_two", ["pusimp_dependency_two"], [False]), # in failure_position # ("pusimp_package_two", ["pusimp_dependency_three"], [True]), # in failure_only_optional # ( # "pusimp_package_two", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True] # ) # in failure_position ("pusimp_package_three", ["pusimp_dependency_two"], [False]), # ("pusimp_package_three", ["pusimp_dependency_three"], [True]), # in failure_only_optional ("pusimp_package_three", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True]), # ("pusimp_package_four", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_five", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_six", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_seven", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_eight", ["pusimp_dependency_five"], [False]), # in failure_position # ("pusimp_package_eight", ["pusimp_dependency_six"], [True]), # in failure_only_optional # ( # "pusimp_package_eight", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True] # ) # in failure_position ("pusimp_package_nine", ["pusimp_dependency_five"], [False]), # ("pusimp_package_nine", ["pusimp_dependency_six"], [True]), # in failure_only_optional ("pusimp_package_nine", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True]) # ( # "pusimp_package_ten", [], [] # ), # pusimp_dependency_missing is not installable, pusimp_dependency_four is always broken ] ) def test_assert_package_import_errors_with_broken_non_optional_packages_data_success( package_name: str, dependencies_import_name: typing.List[str], dependencies_optional: typing.List[bool] ) -> None: """Test success of assert_package_import_errors_with_broken_non_optional_packages on mock packages. The successful cases are the cases in which dependencies_import_name lists actual mandatory dependencies, and the import of the broken dependency happens after the call to pusimp.prevent_user_site_imports. """ assert_package_import_errors_with_broken_non_optional_packages( package_name, dependencies_import_name, dependencies_optional ) @pytest.mark.parametrize( "package_name,dependencies_import_name,dependencies_optional", [ ("pusimp_package_one", ["pusimp_dependency_two"], [False]), ("pusimp_package_two", ["pusimp_dependency_two"], [False]), # ("pusimp_package_two", ["pusimp_dependency_three"], [True]), # in failure_only_optional ("pusimp_package_two", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True]), # ("pusimp_package_three", ["pusimp_dependency_two"], [False]), # in success # ("pusimp_package_three", ["pusimp_dependency_three"], [True]), # in failure_only_optional # ("pusimp_package_three", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True]), # in success # ("pusimp_package_four", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_five", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_six", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_seven", [], []), # pusimp_dependency_four is always broken ("pusimp_package_eight", ["pusimp_dependency_five"], [False]), # ("pusimp_package_eight", ["pusimp_dependency_six"], [True]), # in failure_only_optional ("pusimp_package_eight", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True]) # ("pusimp_package_nine", ["pusimp_dependency_five"], [False]), # in success # ("pusimp_package_nine", ["pusimp_dependency_six"], [True]), # in failure_only_optional # ("pusimp_package_nine", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True]) # in success # ( # "pusimp_package_ten", [], [] # ), # pusimp_dependency_missing is not installable, pusimp_dependency_four is always broken ] ) def test_assert_package_import_errors_with_broken_non_optional_packages_data_failure_position( package_name: str, dependencies_import_name: typing.List[str], dependencies_optional: typing.List[bool] ) -> None: """Test failure of assert_package_import_errors_with_broken_non_optional_packages on mock packages. These failing cases are the cases in which the import of the broken mandatory dependency listed in dependencies_import_name happens before the call to pusimp.prevent_user_site_imports. """ with pytest.raises(AssertionError) as excinfo: assert_package_import_errors_with_broken_non_optional_packages( package_name, dependencies_import_name, dependencies_optional ) assertion_error_text = str(excinfo.value) assert assertion_error_text.startswith( f"'* {dependencies_import_name[0]} is broken' was not found in the ImportError text, namely " f"'Importing {package_name} was not successful" ) assert f"{dependencies_import_name[0]} was purposely broken" in assertion_error_text @pytest.mark.parametrize( "package_name,dependencies_import_name,dependencies_optional", [ # ("pusimp_package_one", ["pusimp_dependency_two"], [False]), # in failure_position # ("pusimp_package_two", ["pusimp_dependency_two"], [False]), # in failure_position ("pusimp_package_two", ["pusimp_dependency_three"], [True]), # ( # "pusimp_package_two", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True] # ) # in failure_position # ("pusimp_package_three", ["pusimp_dependency_two"], [False]), # in success ("pusimp_package_three", ["pusimp_dependency_three"], [True]), # ("pusimp_package_three", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True]), # in success # ("pusimp_package_four", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_five", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_six", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_seven", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_eight", ["pusimp_dependency_five"], [False]), # in failure_position ("pusimp_package_eight", ["pusimp_dependency_six"], [True]), # ( # "pusimp_package_eight", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True] # ), # in failure_position # ("pusimp_package_nine", ["pusimp_dependency_five"], [False]), # in success ("pusimp_package_nine", ["pusimp_dependency_six"], [True]), # ("pusimp_package_nine", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True]) # in success # ( # "pusimp_package_ten", [], [] # ), # pusimp_dependency_missing is not installable, pusimp_dependency_four is always broken ] ) def test_assert_package_import_errors_with_broken_non_optional_packages_data_failure_only_optional( package_name: str, dependencies_import_name: typing.List[str], dependencies_optional: typing.List[bool] ) -> None: """Test failure of assert_package_import_errors_with_broken_non_optional_packages on mock packages. These failing cases are the cases in which only optional dependencies have been listed in dependencies_import_name. """ with pytest.raises(AssertionError) as excinfo: assert_package_import_errors_with_broken_non_optional_packages( package_name, dependencies_import_name, dependencies_optional ) assertion_error_text = str(excinfo.value) assert f"Importing {package_name} was unexpectedly successful" in assertion_error_text @pytest.mark.parametrize( "package_name,dependencies_import_name,dependencies_optional", [ ("pusimp_package_one", ["pusimp_dependency_two"], [False]), ("pusimp_package_two", ["pusimp_dependency_two"], [False]), ("pusimp_package_two", ["pusimp_dependency_three"], [True]), ("pusimp_package_two", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True]), ("pusimp_package_three", ["pusimp_dependency_two"], [False]), ("pusimp_package_three", ["pusimp_dependency_three"], [True]), ("pusimp_package_three", ["pusimp_dependency_two", "pusimp_dependency_three"], [False, True]), # ("pusimp_package_four", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_five", [], []), # pusimp_dependency_missing is not installable # ("pusimp_package_six", [], []), # pusimp_dependency_four is always broken # ("pusimp_package_seven", [], []), # pusimp_dependency_four is always broken ("pusimp_package_eight", ["pusimp_dependency_five"], [False]), ("pusimp_package_eight", ["pusimp_dependency_six"], [True]), ("pusimp_package_eight", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True]), ("pusimp_package_nine", ["pusimp_dependency_five"], [False]), ("pusimp_package_nine", ["pusimp_dependency_six"], [True]), ("pusimp_package_nine", ["pusimp_dependency_five", "pusimp_dependency_six"], [False, True]) # ( # "pusimp_package_ten", [], [] # ), # pusimp_dependency_missing is not installable, pusimp_dependency_four is always broken ] ) def test_assert_package_import_success_with_broken_optional_packages_data( package_name: str, dependencies_import_name: typing.List[str], dependencies_optional: typing.List[bool] ) -> None: """Test success of assert_package_import_success_with_broken_optional_packages on mock packages. The successful cases are the cases in which dependencies_import_name lists actual optional dependencies. """ assert_package_import_success_with_broken_optional_packages( package_name, os.path.join(pusimp_golden_source.system_path, package_name, "__init__.py"), dependencies_import_name, dependencies_optional )