diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..6056a3e84
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,43 @@
+# Editor configuration options.
+# See: https://spec.editorconfig.org/
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[.editorconfig]
+max_line_length = off
+
+[Makefile]
+indent_style = tab
+
+[{*.py,*.pyi}]
+max_line_length = 88
+
+[{*.bash,*.sh,*.zsh}]
+indent_size = 2
+tab_width = 2
+
+[{*.har,*.json,*.json5}]
+indent_size = 2
+max_line_length = off
+
+[{*.markdown,*.md,*.rst}]
+max_line_length = off
+ij_visual_guides = none
+
+[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}]
+max_line_length = off
+
+[{*.ini, *.cfg}]
+max_line_length = off
+
+[{*.yaml,*.yml}]
+indent_size = 2
+max_line_length = off
diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml
index 6d2ee0afc..a6906bd0f 100644
--- a/.github/ISSUE_TEMPLATE/bug.yaml
+++ b/.github/ISSUE_TEMPLATE/bug.yaml
@@ -58,4 +58,3 @@ body:
render: shell
validations:
required: true
-
diff --git a/.github/workflows/snap.yaml b/.github/workflows/snap.yaml
index dcdeb50bf..27ffd426f 100644
--- a/.github/workflows/snap.yaml
+++ b/.github/workflows/snap.yaml
@@ -32,7 +32,7 @@ jobs:
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]
then
- echo "branch=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT"
+ echo "branch=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT"
else
branch=$(echo ${GITHUB_REF#refs/*/} | sed -e 's|feature/\(.*\)|\1|')
echo "branch=$branch" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/spread-large.yaml b/.github/workflows/spread-large.yaml
index 894ec0c6c..e85cb41f6 100644
--- a/.github/workflows/spread-large.yaml
+++ b/.github/workflows/spread-large.yaml
@@ -1,9 +1,9 @@
name: Spread (large)
on:
pull_request:
- types: [ labeled ]
+ types: [labeled]
schedule:
- - cron: "0 2 * * 0,3" # run at 2 AM on Sundays and Wednesdays
+ - cron: "0 2 * * 0,3" # run at 2 AM on Sundays and Wednesdays
jobs:
snap-build:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index ce7a6fae2..20edf7719 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,108 +1,143 @@
name: Tests
on:
- pull_request:
push:
branches:
- - main
+ - "main"
+ - "feature/*"
+ - "hotfix/*"
+ - "release/*"
+ - "renovate/*"
+ pull_request:
jobs:
- linters:
+ lint:
runs-on: ubuntu-22.04
steps:
- - name: Checkout code
+ - name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
+ - name: Conventional commits
+ uses: webiny/action-conventional-commits@v1.1.0
- name: Install packages
run: |
sudo apt update
sudo apt install -y libapt-pkg-dev aspell aspell-en
- - name: Set up Python 3.10
+ - name: Setup Python
uses: actions/setup-python@v4
with:
- python-version: "3.10"
- - name: Install python packages and dependencies
- run: |
- pip install -U wheel -r requirements-jammy.txt -r requirements.txt -r requirements-dev.txt
- pip install -e .
- - name: Run black
- run: |
- make test-black
- - name: Run codespell
- run: |
- make test-codespell
- - name: Run flake8
- run: |
- make test-flake8
- - name: Run isort
- run: |
- make test-isort
- - name: Run mypy
- run: |
- make test-mypy
- - name: Run pydocstyle
- run: |
- make test-pydocstyle
- - name: Run pyright
- run: |
- sudo snap install --classic node
- sudo snap install --classic pyright
- make test-pyright
- - name: Run pylint
- run: |
- make test-pylint
- - name: Run sphinx-lint
- run: |
- make test-sphinx-lint
- - name: Run shellcheck
- run: |
- sudo snap install shellcheck
- make test-shellcheck
- - name: Run linkcheck,woke,spelling
- run: |
- sudo snap install woke
- make test-docs
-
- tests:
+ python-version: '3.10'
+ cache: 'pip'
+ - name: Configure environment
+ run: |
+ echo "::group::Begin snap install"
+ echo "Installing snaps in the background while running apt and pip..."
+ sudo snap install --no-wait --classic pyright
+ sudo snap install --no-wait shellcheck
+ echo "::endgroup::"
+ echo "::group::pip install"
+ python -m pip install tox
+ echo "::endgroup::"
+ echo "::group::Create virtual environments for linting processes."
+ tox run --colored yes -m lint --notest
+ echo "::endgroup::"
+ echo "::group::Wait for snap to complete"
+ snap watch --last=install
+ echo "::endgroup::"
+ - name: Run Linters
+ run: tox run --skip-pkg-install --no-list-dependencies --colored yes -m lint
+ unit:
strategy:
matrix:
- os: [ubuntu-22.04, windows-2019]
- python-version: ["3.10", "3.12"]
-
- runs-on: ${{ matrix.os }}
+ platform: [ubuntu-22.04, windows-2019]
+ runs-on: ${{ matrix.platform }}
steps:
- - name: Checkout code
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
+ - name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: ${{ matrix.python-version }}
- - name: Install python packages and dependencies
- run: |
- pip install -U wheel
+ python-version: |
+ 3.10
+ 3.12
+ cache: 'pip'
- name: Install Ubuntu-specific dependencies
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
+ if: ${{ startsWith(matrix.platform, 'ubuntu') }}
run: |
sudo apt update
- sudo apt install -y python3-pip python3-setuptools python3-wheel python3-venv libapt-pkg-dev
+ sudo apt install -y libapt-pkg-dev
- name: Install Ubuntu 22.04-specific dependencies
- if: ${{ matrix.os == 'ubuntu-22.04' }}
+ if: ${{ matrix.platform == 'ubuntu-22.04' }}
run: |
- pip install -U -r requirements-jammy.txt
# 22.04 is the only one that has an 'umoci' package
sudo apt install -y umoci
- - name: Install dependencies
- run: |
- pip install -U -r requirements.txt -r requirements-dev.txt
- pip install -e .
- - name: Run unit tests
+ - name: Configure environment
+ run: |
+ echo "::group::pip install"
+ python -m pip install tox
+ echo "::endgroup::"
+ mkdir -p results
+ - name: Setup Tox environments
+ run: tox run --colored yes -m tests --notest
+ - name: Test with tox
+ run: tox run --skip-pkg-install --no-list-dependencies --result-json results/tox-${{ matrix.platform }}.json --colored yes -m unit-tests
+ env:
+ PYTEST_ADDOPTS: "--no-header -vv -rN"
+ - name: Upload code coverage
+ uses: codecov/codecov-action@v3
+ with:
+ directory: ./results/
+ files: coverage*.xml
+ - name: Upload test results
+ if: success() || failure()
+ uses: actions/upload-artifact@v3
+ with:
+ name: test-results-${{ matrix.platform }}
+ path: results/
+ integration:
+ strategy:
+ matrix:
+ platform: [ubuntu-22.04, windows-2019]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: |
+ 3.10
+ 3.12
+ cache: 'pip'
+ - name: Install Ubuntu-specific dependencies
+ if: ${{ startsWith(matrix.platform, 'ubuntu') }}
run: |
- make test-units
- - name: Run integration tests
+ sudo apt update
+ sudo apt install -y libapt-pkg-dev
+ - name: Install Ubuntu 22.04-specific dependencies
+ if: ${{ matrix.platform == 'ubuntu-22.04' }}
run: |
- make test-integrations
-
+ # 22.04 is the only one that has an 'umoci' package
+ sudo apt install -y umoci
+ - name: Configure environment
+ run: |
+ echo "::group::pip install"
+ python -m pip install tox
+ echo "::endgroup::"
+ mkdir -p results
+ - name: Setup Tox environments
+ run: tox run --colored yes -m tests --notest
+ - name: Test with tox
+ run: tox run --skip-pkg-install --no-list-dependencies --result-json results/tox-${{ matrix.platform }}.json --colored yes -m integration-tests
+ env:
+ PYTEST_ADDOPTS: "--no-header -vv -rN"
+ - name: Upload test results
+ if: success() || failure()
+ uses: actions/upload-artifact@v3
+ with:
+ name: test-results-${{ matrix.platform }}
+ path: results/
diff --git a/.gitignore b/.gitignore
index 1149d9bc9..f30802502 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,9 +51,6 @@ coverage.xml
.hypothesis/
.pytest_cache/
-# Spread tests
-.spread-reuse.yaml
-
# Translations
*.mo
*.pot
@@ -73,6 +70,8 @@ instance/
# Sphinx documentation
docs/_build/
+docs/reference/commands
+docs/sphinx-starter-pack/
# PyBuilder
target/
@@ -131,27 +130,28 @@ dmypy.json
# Pyre type checker
.pyre/
-# IDE settings
-.vscode/
+
+# Caches for various tools
+/.*_cache/
+
+# Test results
+/results/
# direnv
.direnv
.envrc
-# snap
-rockcraft_*.snap
+# Ignore version module generated by setuptools_scm
+/*/_version.py
-# rock
-*.rock
+# Visual Studio Code
+.vscode/
-# sphinx
+# Ignore external tools
+/tools/external/
-docs/warnings.txt
-docs/.sphinx/.wordlist.dic
-docs/reference/commands
-.DS_Store
-__pycache__
-.idea/
+# snap
+rockcraft_*.snap
-# build-generated version file
-rockcraft/_version.py
+# rock
+*.rock
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..a3302107c
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,30 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+ - id: check-merge-conflict
+ - id: check-toml
+ - id: fix-byte-order-marker
+ - id: mixed-line-ending
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
+ # renovate: datasource=pypi;depName=ruff
+ rev: "v0.1.6"
+ hooks:
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix]
+ - repo: https://github.com/psf/black
+ # renovate: datasource=pypi;depName=black
+ rev: "23.11.0"
+ hooks:
+ - id: black
+ - repo: https://github.com/adrienverge/yamllint.git
+ # renovate: datasource=pypi;depName=yamllint
+ rev: "v1.33.0"
+ hooks:
+ - id: yamllint
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index e48636709..fb5811702 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -1,39 +1,27 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
----
+
# Required
version: 2
-build:
- os: ubuntu-22.04
- tools:
- python: "3.10"
- jobs:
- post_checkout:
- - git submodule update --init -- docs/sphinx-starter-pack
- - make preparedocs
- post_install:
- - ./tools/mkdoclinks.sh
-
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
- builder: dirhtml
- fail_on_warning: true
-
-# Build documentation with MkDocs
-# mkdocs:
-# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
+ - epub
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3"
-# Optionally set the version of Python
-# and requirements required to build your docs
python:
install:
- - requirements: requirements-doc.txt
- method: pip
path: .
+ extra_requirements:
+ - docs
diff --git a/.yamllint.yaml b/.yamllint.yaml
new file mode 100644
index 000000000..8f7da7486
--- /dev/null
+++ b/.yamllint.yaml
@@ -0,0 +1,12 @@
+---
+ignore-from-file: [.gitignore]
+
+extends: default
+
+rules:
+ document-start: disable
+ float-values: enable
+ line-length: disable
+ octal-values: enable
+ truthy:
+ check-keys: false
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9fd8c6c8d..000000000
--- a/Makefile
+++ /dev/null
@@ -1,148 +0,0 @@
-.PHONY: help
-help: ## Show this help.
- @printf "%-40s %s\n" "Target" "Description"
- @printf "%-40s %s\n" "------" "-----------"
- @fgrep " ## " $(MAKEFILE_LIST) | fgrep -v grep | awk -F ': .*## ' '{$$1 = sprintf("%-40s", $$1)} 1'
-
-.PHONY: autoformat
-autoformat: ## Run automatic code formatters.
- isort .
- autoflake rockcraft/ tests/
- black .
- ruff check --fix-only rockcraft tests
-
-.PHONY: clean
-clean: ## Clean artefacts from building, testing, etc.
- rm -rf build/
- rm -rf dist/
- rm -rf .eggs/
- find . -name '*.egg-info' -exec rm -rf {} +
- find . -name '*.egg' -exec rm -f {} +
- rm -rf docs/_build/
- rm -f docs/rockcraft.*
- rm -f docs/modules.rst
- rm -rf docs/reference/commands
- find . -name '*.pyc' -exec rm -f {} +
- find . -name '*.pyo' -exec rm -f {} +
- find . -name '*~' -exec rm -f {} +
- find . -name '__pycache__' -exec rm -rf {} +
- rm -rf .tox/
- rm -f .coverage
- rm -rf htmlcov/
- rm -rf .pytest_cache
- $(MAKE) -C docs clean
- rm -rf .mypy_cache
-
-.PHONY: coverage
-coverage: ## Run pytest with coverage report.
- coverage run --source craft_sore -m pytest
- coverage report -m
- coverage html
-
-.PHONY: preparedocs
-preparedocs: ## move file from the sphinx-starter-pack to docs folder
- cp docs/sphinx-starter-pack/.sphinx/_static/* docs/_static
- mkdir -p docs/_templates
- cp -R docs/sphinx-starter-pack/.sphinx/_templates/* docs/_templates
- cp docs/sphinx-starter-pack/.sphinx/spellingcheck.yaml docs/spellingcheck.yaml
-
-.PHONY: installdocs
-installdocs: preparedocs ## install documentation dependencies.
- $(MAKE) -C docs install
-
-.PHONY: docs
-docs: ## Generate documentation.
- rm -f docs/rockcraft.rst
- rm -f docs/modules.rst
- $(MAKE) -C docs clean-doc
- $(MAKE) -C docs html
-
-.PHONY: rundocs
-rundocs: ## start a documentation runserver
- $(MAKE) -C docs run
-
-.PHONY: dist
-dist: clean ## Build python package.
- python setup.py sdist
- python setup.py bdist_wheel
- ls -l dist
-
-.PHONY: freeze-requirements
-freeze-requirements: ## Re-freeze requirements.
- tools/freeze-requirements.sh
-
-.PHONY: install
-install: clean ## Install python package.
- python setup.py install
-
-.PHONY: lint
-lint: test-black test-codespell test-flake8 test-isort test-mypy test-pydocstyle test-pyright test-pylint test-sphinx-lint test-shellcheck ## Run all linting tests.
-
-.PHONY: release
-release: dist ## Release with twine.
- twine upload dist/*
-
-.PHONY: test-black
-test-black:
- black --check --diff .
-
-.PHONY: test-codespell
-test-codespell:
- codespell .
-
-.PHONY: test-flake8
-test-flake8:
- flake8 rockcraft tests
-
-.PHONY: test-ruff
-test-ruff:
- ruff rockcraft tests
-
-.PHONY: test-integrations
-test-integrations: ## Run integration tests.
- pytest tests/integration
-
-.PHONY: test-isort
-test-isort:
- isort --check rockcraft tests
-
-.PHONY: test-mypy
-test-mypy:
- mypy rockcraft tests
-
-.PHONY: test-pydocstyle
-test-pydocstyle:
- pydocstyle rockcraft
-
-.PHONY: test-pylint
-test-pylint:
- pylint rockcraft
- pylint tests --disable=invalid-name,missing-module-docstring,missing-function-docstring,redefined-outer-name,too-many-arguments,too-many-public-methods,no-member,import-outside-toplevel
-
-.PHONY: test-pyright
-test-pyright:
- pyright .
-
-.PHONY: test-shellcheck
-test-shellcheck:
- # shellcheck for shell scripts
- git ls-files | file --mime-type -Nnf- | grep shellscript | cut -f1 -d: | xargs shellcheck
- # shellcheck for bash commands inside spread task.yaml files
- tools/external/utils/spread-shellcheck tests/spread/ spread.yaml
-
-.PHONY: test-sphinx-lint
-test-sphinx-lint:
- sphinx-lint --ignore docs/sphinx-starter-pack/ --ignore docs/_build --ignore docs/env --max-line-length 80 -e all docs/*
-
-.PHONY: test-units
-test-units: ## Run unit tests.
- pytest tests/unit
-
-.PHONY: test-docs
-test-docs: installdocs ## Run docs tests.
- $(MAKE) -C docs linkcheck
- $(MAKE) -C docs woke
- $(MAKE) -C docs spelling
-
-.PHONY: tests
-tests: lint test-integrations test-units test-docs ## Run all tests.
diff --git a/README.rst b/README.rst
index 0f46721ab..0171c7b64 100644
--- a/README.rst
+++ b/README.rst
@@ -8,7 +8,8 @@ Rockcraft
Purpose
-------
-Tool to create OCI Images using the language from `Snapcraft`_ and `Charmcraft`_.
+Tool to create OCI Images using the language from `Snapcraft`_ and
+`Charmcraft`_.
.. _Snapcraft: https://snapcraft.io
@@ -44,13 +45,14 @@ https://canonical-rockcraft.readthedocs-hosted.com/
:target: https://snapcraft.io/rockcraft
-
+
Testing
-------
-In addition to unit tests in :code:`tests/unit`, which can be run with :code:`make test-units`,
-a number of integrated tests in :code:`tests/spread` can be run with `Spread`_. See the
-`general notes`_ and take note of these ``rockcraft``-specific instructions:
+In addition to unit tests in :code:`tests/unit`, which can be run with
+:code:`make test-units`, a number of integrated tests in :code:`tests/spread`
+can be run with `Spread`_. See the `general notes`_ and take note of these
+``rockcraft``-specific instructions:
* Initialize/update git submodules to fetch Spread-related helper scripts:
diff --git a/docs/_static/favicon.png b/docs/_static/favicon.png
new file mode 100644
index 000000000..316643d6d
Binary files /dev/null and b/docs/_static/favicon.png differ
diff --git a/docs/how-to/build-docs.rst b/docs/how-to/build-docs.rst
index 7b83c57e7..e297524f2 100644
--- a/docs/how-to/build-docs.rst
+++ b/docs/how-to/build-docs.rst
@@ -3,21 +3,13 @@
How to build the documentation
******************************
-Use the provided ``Makefile`` to install the documentation requirements:
-
-.. literalinclude:: code/build-docs/task.yaml
- :language: bash
- :start-after: [docs:install-deps]
- :end-before: [docs:install-deps-end]
- :dedent: 2
-
-Once the requirements are installed, you can use the provided ``Makefile`` to
+Use the provided ``tox.ini`` to install the documentation requirements and
build the documentation:
.. literalinclude:: code/build-docs/task.yaml
:language: bash
- :start-after: [docs:make-docs]
- :end-before: [docs:make-docs-end]
+ :start-after: [docs:build-docs]
+ :end-before: [docs:build-docs-end]
:dedent: 2
Even better, serve it locally on port 8080. The documentation will be rebuilt
@@ -26,8 +18,7 @@ on each file change, and will reload the browser view.
.. literalinclude:: code/build-docs/task.yaml
:language: bash
:start-after: timeout -s SIGINT
- :end-before: [docs:make-rundocs-end]
+ :end-before: [docs:build-autobuild-end]
:dedent: 2
-Note that ``make rundocs`` automatically activates the virtual environment,
-as long as it already exists.
+Note that ``tox`` automatically creates and activates the virtual environment.
diff --git a/docs/how-to/code/build-docs/task.yaml b/docs/how-to/code/build-docs/task.yaml
index dbb1a4d94..03b05762d 100644
--- a/docs/how-to/code/build-docs/task.yaml
+++ b/docs/how-to/code/build-docs/task.yaml
@@ -1,30 +1,31 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the "Build the docs" guide
prepare: |
- tests.pkgs install python3-venv python3-dev libapt-pkg-dev
+ tests.pkgs install tox libapt-pkg-dev
execute: |
pushd $PROJECT_PATH/
- # [docs:install-deps]
- make installdocs
- # [docs:install-deps-end]
-
- # [docs:make-docs]
- make docs # the home page can be found at docs/_build/html/index.html
- # [docs:make-docs-end]
+ # create git for getting version
+ git init
+ git add .
+ git commit -m "test"
+
+ # [docs:build-docs]
+ tox -e build-docs # the home page can be found at docs/_build/html/index.html
+ # [docs:build-docs-end]
set +e
- timeout -s SIGINT 7 \
- make rundocs
- # [docs:make-rundocs-end]
+ timeout -s SIGINT 60 \
+ tox -e autobuild-docs
+ # [docs:build-autobuild-end]
ret=$?
set -e
diff --git a/docs/how-to/code/convert-to-pebble-layer/rockcraft.yaml b/docs/how-to/code/convert-to-pebble-layer/rockcraft.yaml
index 6aa6b300a..430eed349 100644
--- a/docs/how-to/code/convert-to-pebble-layer/rockcraft.yaml
+++ b/docs/how-to/code/convert-to-pebble-layer/rockcraft.yaml
@@ -3,16 +3,16 @@ base: "ubuntu@22.04"
version: latest
summary: An NGINX ROCK
description: |
- A ROCK equivalent of the official NGINX Docker image from Docker Hub.
+ A ROCK equivalent of the official NGINX Docker image from Docker Hub.
license: Apache-2.0
platforms:
- amd64:
+ amd64:
package-repositories:
- type: apt
url: https://nginx.org/packages/mainline/ubuntu
key-id: 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
- suites:
+ suites:
- jammy
components:
- nginx
@@ -49,4 +49,3 @@ services:
environment:
TZ: UTC
on-failure: shutdown
-
\ No newline at end of file
diff --git a/docs/how-to/code/convert-to-pebble-layer/task.yaml b/docs/how-to/code/convert-to-pebble-layer/task.yaml
index ee2ba0a1f..c5bf4aac4 100644
--- a/docs/how-to/code/convert-to-pebble-layer/task.yaml
+++ b/docs/how-to/code/convert-to-pebble-layer/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the "How to convert a popular entrypoint to a Pebble layer" guide
@@ -27,4 +27,4 @@ execute: |
# [docs:curl-end]
docker rm -f nginx-pebble-service
- docker rmi -f custom-nginx-rock:latest
\ No newline at end of file
+ docker rmi -f custom-nginx-rock:latest
diff --git a/docs/how-to/code/create-slice/openssl.yaml b/docs/how-to/code/create-slice/openssl.yaml
index 3de2ff4b0..9ed4f9da0 100644
--- a/docs/how-to/code/create-slice/openssl.yaml
+++ b/docs/how-to/code/create-slice/openssl.yaml
@@ -2,10 +2,10 @@ package: openssl
slices:
bins:
essential:
- - libc6_libs
- - libc6_config
- - libssl3_libs
- - openssl_config
+ - libc6_libs
+ - libc6_config
+ - libssl3_libs
+ - openssl_config
contents:
/usr/bin/c_rehash:
/usr/bin/openssl:
diff --git a/docs/how-to/code/create-slice/task.yaml b/docs/how-to/code/create-slice/task.yaml
index f87127ee8..8571c4683 100644
--- a/docs/how-to/code/create-slice/task.yaml
+++ b/docs/how-to/code/create-slice/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the "Create a package slice for Chisel" guide
diff --git a/docs/how-to/code/get-started/task.yaml b/docs/how-to/code/get-started/task.yaml
index 6d2963856..98cd298a1 100644
--- a/docs/how-to/code/get-started/task.yaml
+++ b/docs/how-to/code/get-started/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the steps for getting started with Rockcraft
@@ -13,7 +13,7 @@ execute: |
# [docs:snap-version-end]
# [docs:lxd-version]
- lxd --version
+ lxd --version
# [docs:lxd-version-end]
# [docs:lxd-status]
diff --git a/docs/how-to/code/install-slice/task.yaml b/docs/how-to/code/install-slice/task.yaml
index e729d9cb4..c5fbd2a8d 100644
--- a/docs/how-to/code/install-slice/task.yaml
+++ b/docs/how-to/code/install-slice/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the "Install a custom package slice" guide
diff --git a/docs/how-to/code/publish-slice/task.yaml b/docs/how-to/code/publish-slice/task.yaml
index e1da7f4dc..69b6fdcdf 100644
--- a/docs/how-to/code/publish-slice/task.yaml
+++ b/docs/how-to/code/publish-slice/task.yaml
@@ -1,15 +1,15 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the "Release a slice definitions file" guide
execute: |
git clone -b ubuntu-22.04 https://github.com/canonical/chisel-releases.git
-
+
# [docs:new-branch]
cd chisel-releases
git checkout -b create-openssl-bins-slice
diff --git a/docs/how-to/code/rockcraft-pack-action/task.yaml b/docs/how-to/code/rockcraft-pack-action/task.yaml
index 3c72b3f46..9b5658fe1 100644
--- a/docs/how-to/code/rockcraft-pack-action/task.yaml
+++ b/docs/how-to/code/rockcraft-pack-action/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test the "Use the GitHub Action" guide
@@ -11,4 +11,4 @@ prepare: |
tests.pkgs install yamllint
execute: |
- yamllint rockcraft-pack.yaml
\ No newline at end of file
+ yamllint rockcraft-pack.yaml
diff --git a/docs/reference/code/example/task.yaml b/docs/reference/code/example/task.yaml
index d3af7b4c1..313b96e29 100644
--- a/docs/reference/code/example/task.yaml
+++ b/docs/reference/code/example/task.yaml
@@ -1,4 +1,3 @@
-
summary: Check that we can build the example rockcraft.yaml
execute: |
diff --git a/docs/tutorials/code/chisel/task.yaml b/docs/tutorials/code/chisel/task.yaml
index 0371ddf79..23bd8374f 100644
--- a/docs/tutorials/code/chisel/task.yaml
+++ b/docs/tutorials/code/chisel/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: test for the "Install packages slices into a ROCK" tutorial
@@ -15,7 +15,7 @@ execute: |
# [docs:build-rock]
rockcraft pack
# [docs:build-rock-end]
-
+
test -f chisel-openssl_0.0.1_amd64.rock
# [docs:skopeo-copy]
@@ -29,5 +29,5 @@ execute: |
# [docs:docker-run-with-args]
docker run --rm chisel-openssl exec --env=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt openssl s_client -connect ubuntu.com:443 -brief
# [docs:docker-run-with-args-end]
-
+
docker run --rm chisel-openssl exec --env=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt openssl s_client -connect ubuntu.com:443 -brief 2>&1 | grep "Verification: OK"
diff --git a/docs/tutorials/code/hello-world/task.yaml b/docs/tutorials/code/hello-world/task.yaml
index c7db9fa92..915801911 100644
--- a/docs/tutorials/code/hello-world/task.yaml
+++ b/docs/tutorials/code/hello-world/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: hello world tutorial
@@ -36,5 +36,5 @@ execute: |
# [docs:docker-run]
docker run --rm hello:1.0 exec hello -t
# [docs:docker-run-end]
-
+
docker run --rm hello:1.0 exec hello -t | grep "hello, world"
diff --git a/docs/tutorials/code/migrate-to-chiselled-rock/rockcraft.yaml b/docs/tutorials/code/migrate-to-chiselled-rock/rockcraft.yaml
index 8ce8c384f..f0233a13c 100644
--- a/docs/tutorials/code/migrate-to-chiselled-rock/rockcraft.yaml
+++ b/docs/tutorials/code/migrate-to-chiselled-rock/rockcraft.yaml
@@ -25,7 +25,7 @@ parts:
stage-packages:
- base-files_base
- dotnet-runtime-6.0_libs
-
+
# Based on requirement R4, create the symbolic link
override-prime: |
craftctl default
diff --git a/docs/tutorials/code/migrate-to-chiselled-rock/task.yaml b/docs/tutorials/code/migrate-to-chiselled-rock/task.yaml
index 3b28d9791..a734b0d70 100644
--- a/docs/tutorials/code/migrate-to-chiselled-rock/task.yaml
+++ b/docs/tutorials/code/migrate-to-chiselled-rock/task.yaml
@@ -1,8 +1,8 @@
###########################################
# IMPORTANT
# Comments matter!
-# The docs use the wrapping comments as
-# markers for including said instructions
+# The docs use the wrapping comments as
+# markers for including said instructions
# as snippets in the docs.
###########################################
summary: docker image migration tutorial
@@ -21,7 +21,7 @@ execute: |
# [docs:build-docker-image]
docker build -t dotnet-runtime:reference .
# [docs:build-docker-image-end]
-
+
popd
# [docs:inspect-docker-image]
@@ -48,7 +48,7 @@ execute: |
# [docs:skopeo-copy]
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:dotnet-runtime_chiselled_amd64.rock docker-daemon:dotnet-runtime:chiselled
# [docs:skopeo-copy-end]
-
+
# [docs:inspect-rock]
docker images dotnet-runtime:chiselled
# [docs:inspect-rock-end]
@@ -56,7 +56,7 @@ execute: |
# [docs:run-rock]
docker run --rm dotnet-runtime:chiselled exec dotnet --info
# [docs:run-rock-end]
-
+
restore: |
rm dotnet-runtime_chiselled_amd64.rock
docker rmi -f dotnet-runtime:chiselled dotnet-runtime:reference
diff --git a/docs/tutorials/code/node-app/rockcraft.yaml b/docs/tutorials/code/node-app/rockcraft.yaml
index e2bbe66ef..5200f3491 100644
--- a/docs/tutorials/code/node-app/rockcraft.yaml
+++ b/docs/tutorials/code/node-app/rockcraft.yaml
@@ -9,17 +9,17 @@ platforms:
amd64:
services:
- app:
- override: replace
- command: node server.js
- startup: enabled
- on-success: shutdown
- on-failure: shutdown
- working-dir: /lib/node_modules/node_web_app
+ app:
+ override: replace
+ command: node server.js
+ startup: enabled
+ on-success: shutdown
+ on-failure: shutdown
+ working-dir: /lib/node_modules/node_web_app
parts:
- app:
- plugin: npm
- npm-include-node: True
- npm-node-version: "21.1.0"
- source: src/
+ app:
+ plugin: npm
+ npm-include-node: true
+ npm-node-version: "21.1.0"
+ source: src/
diff --git a/docs/tutorials/code/node-app/task.yaml b/docs/tutorials/code/node-app/task.yaml
index 2d225940d..60caff41b 100644
--- a/docs/tutorials/code/node-app/task.yaml
+++ b/docs/tutorials/code/node-app/task.yaml
@@ -1,4 +1,3 @@
-
summary: nodejs tutorial
execute: |
diff --git a/docs/tutorials/code/pyfiglet/task.yaml b/docs/tutorials/code/pyfiglet/task.yaml
index 4d592bc43..62eae7cc7 100644
--- a/docs/tutorials/code/pyfiglet/task.yaml
+++ b/docs/tutorials/code/pyfiglet/task.yaml
@@ -11,12 +11,12 @@ execute: |
# [docs:create-pyfiglet-dir]
mkdir pyfiglet-rock && cd pyfiglet-rock
# [docs:create-pyfiglet-dir-end]
-
+
cp ../rockcraft.yaml .
-
+
# [docs:build-rock]
rockcraft pack
- # [docs:build-rock-end]
+ # [docs:build-rock-end]
# [docs:skopeo-copy]
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:pyfiglet_0.7.6_amd64.rock docker-daemon:pyfiglet:0.7.6
diff --git a/pyproject.toml b/pyproject.toml
index b4aa0873c..7ee41891d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,69 @@
+[project]
+name = "rockcraft"
+description = "Create ROCKS"
+dynamic = ["dependencies", "readme", "version"]
+license = {file = "LICENSE"}
+authors = [
+ {name = "Canonical Ltd.", email = "snapcraft@lists.snapcraft.io"}
+]
+
+classifiers = [
+ "Development Status :: 2 - Pre-Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: POSIX :: Linux",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+]
+requires-python = ">=3.10"
+
+[project.scripts]
+rockcraft = "rockcraft.cli:run"
+
+[project.urls]
+documentation = "https://rockcraft.readthedocs.io/en/latest/"
+source = "https://github.com/canonical/rockcraft.git"
+issues = "https://github.com/canonical/rockcraft/issues"
+
+[project.optional-dependencies]
+dev = [
+ "build",
+ "coverage[toml]==7.3.2",
+ "pytest==7.4.3",
+ "pytest-check>=2.0",
+ "pytest-cov==4.1.0",
+ "pytest-mock==3.12.0",
+ "pytest-subprocess",
+]
+ubuntu-jammy = [
+ "python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.4.0ubuntu1/python-apt_2.4.0ubuntu1.tar.xz ; sys_platform=='linux'"
+]
+lint = [
+ "black==23.11.0",
+ "codespell[toml]==2.2.6",
+ "ruff==0.1.6",
+ "yamllint==1.33.0"
+]
+types = [
+ "mypy[reports]==1.7.1",
+ "pyright==1.1.337",
+ "types-requests",
+ "types-setuptools",
+ "types-pyyaml",
+ "types-tabulate>=0.9.0.2",
+]
+docs = [
+ "furo==2023.9.10",
+ "sphinx>=7.1.2,<8",
+ "sphinx-autobuild==2021.3.14",
+ "sphinx-copybutton==0.5.2",
+ "sphinx-design==0.5.0",
+ "sphinx-pydantic==0.1.1",
+ "sphinx-toolbox==3.5.0",
+ "sphinx-lint==0.9.0",
+]
+
[build-system]
requires = [
"setuptools==67.7.2",
@@ -5,6 +71,12 @@ requires = [
]
build-backend = "setuptools.build_meta"
+[tool.setuptools.dynamic]
+dependencies = {file = ["requirements.txt"]}
+readme = {file = "README.rst"}
+version = {attr = "rockcraft.__version__"}
+
+
[tool.setuptools_scm]
write_to = "rockcraft/_version.py"
# the version comes from the latest annotated git tag formatted as 'X.Y.Z'
@@ -22,6 +94,19 @@ version_scheme = "post-release"
# - only match tags formatted as 'X.Y.Z'
git_describe_command = "git describe --dirty --long --match '[0-9]*.[0-9]*.[0-9]*' --exclude '*[^0-9.]*'"
+[tool.setuptools.packages.find]
+include = ["*craft*"]
+namespaces = false
+
+[tool.black]
+target-version = ["py310"]
+
+[tool.codespell]
+ignore-words-list = "buildd,crate,keyserver,comandos,ro,dedent,dedented"
+skip = ".tox,.git,build,.*_cache,__pycache__,*.tar,*.snap,*.png,./node_modules,./docs/_build,.direnv,.venv,venv,.vscode"
+quiet-level = 3
+check-filenames = true
+
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
@@ -30,32 +115,58 @@ use_parentheses = true
ensure_newline_before_comments = true
line_length = 88
-[tool.black]
-# Use extend-exclude so that the default excludes (including those from .gitignore)
-# are not overwritten.
-extend-exclude = "docs/sphinx-starter-pack"
+[tool.pytest.ini_options]
+minversion = "7.0"
+testpaths = "tests"
+xfail_strict = true
+markers = [
+ "notox: tests that will not work in tox env",
+]
+
+[tool.coverage.run]
+branch = true
+parallel = true
+omit = ["tests/**"]
+
+[tool.coverage.report]
+skip_empty = true
+exclude_also = [
+ "if (typing\\.)?TYPE_CHECKING:",
+]
[tool.pyright]
-ignore = ["docs/sphinx-starter-pack"]
+strict = ["rockcraft"]
+pythonVersion = "3.10"
+pythonPlatform = "Linux"
+exclude = [
+ "**/.*",
+ "**/__pycache__",
+ # pyright might not like the annotations generated by setuptools_scm
+ "**/_version.py",
+ # sphinx-starter-pack not under control
+ "docs/sphinx-starter-pack/",
+]
[tool.mypy]
python_version = "3.10"
exclude = [
"build",
+ "tests",
"results",
- "tests/spread"
+ # sphinx-starter-pack not under control
+ "docs",
]
warn_unused_configs = true
warn_redundant_casts = true
strict_equality = true
strict_concatenate = true
-#warn_return_any = true
+warn_return_any = true
disallow_subclassing_any = true
disallow_untyped_decorators = true
-#disallow_any_generics = true
+disallow_any_generics = true
[[tool.mypy.overrides]]
-module = ["rockcraft"]
+module = ["rockcraft.*"]
disallow_untyped_defs = true
no_implicit_optional = true
@@ -63,37 +174,13 @@ no_implicit_optional = true
module = ["tests.*"]
strict = false
-[[tool.mypy.overrides]]
-module = ["craft_archives"]
-ignore_missing_imports = true
-
-[tool.pylint.messages_control]
-disable = "too-many-ancestors,too-few-public-methods,fixme,unspecified-encoding,use-implicit-booleaness-not-comparison,unnecessary-lambda-assignment,line-too-long,cyclic-import"
-
-[tool.pylint.similarities]
-min-similarity-lines=12
-
-[tool.pylint.format]
-max-line-length = "88"
-max-locals = 16
-
-[tool.pylint.MASTER]
-extension-pkg-whitelist = [
- "pydantic"
-]
-load-plugins = "pylint_pytest"
-ignore-paths = [
- "rockcraft/_version.py"
-]
-
[tool.ruff]
line-length = 88
-target-version = "py38"
+target-version = "py310"
src = ["rockcraft", "tests"]
extend-exclude = [
"docs",
"__pycache__",
- "rockcraft/_version.py"
]
# Follow ST063 - Maintaining and updating linting specifications for updating these.
select = [ # Base linting rule selections.
@@ -103,49 +190,74 @@ select = [ # Base linting rule selections.
# failures with ruff updates.
"F", # The rules built into Flake8
"E", "W", # pycodestyle errors and warnings
- "D", # Implement pydocstyle checking as well.
"I", # isort checking
- "PLC", "PLE", "PLR", "PLW", # Pylint
"N", # PEP8 naming
+ "D", # Implement pydocstyle checking as well.
+ "UP", # Pyupgrade - note that some of are excluded below due to Python versions
"YTT", # flake8-2020: Misuse of `sys.version` and `sys.version_info`
+ "ANN", # Type annotations.
+ "ASYNC", # Catching blocking calls in async functions
+ # flake8-bandit: security testing. https://docs.astral.sh/ruff/rules/#flake8-bandit-s
+ # https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing
+ "S101", "S102", # assert or exec
+ "S103", "S108", # File permissions and tempfiles - use #noqa to silence when appropriate.
+ "S104", # Network binds
+ "S105", "S106", "S107", # Hardcoded passwords
+ "S110", # try-except-pass (use contextlib.suppress instead)
+ "S113", # Requests calls without timeouts
+ "S3", # Serialising, deserialising, hashing, crypto, etc.
+ "S5", # Unsafe cryptography or YAML loading.
+ "S602", # Subprocess call with shell=true
+ "S701", # jinja2 templates without autoescape
"BLE", # Do not catch blind exceptions
"FBT", # Disallow boolean positional arguments (make them keyword-only)
+ "B0", # Common mistakes and typos.
"A", # Shadowing built-ins.
+ "COM", # Trailing commas
"C4", # Encourage comprehensions, which tend to be faster than alternatives.
"T10", # Don't call the debugger in production code
"ISC", # Implicit string concatenation that can cause subtle issues
"ICN", # Only use common conventions for import aliases.
+ "INP", # Implicit namespace packages
+ # flake8-pie: miscellaneous linters (enabled individually because they're not really related)
+ "PIE790", # Unnecessary pass statement
+ "PIE794", # Multiple definitions of class field
+ "PIE796", # Duplicate value in an enum (reasonable to noqa for backwards compatibility)
+ "PIE804", # Don't use a dict with unnecessary kwargs
+ "PIE807", # prefer `list` over `lambda: []`
+ "PIE810", # Use a tuple rather than multiple calls. E.g. `mystr.startswith(("Hi", "Hello"))`
+ "PYI", # Linting for type stubs.
+ "PT", # Pytest
"Q", # Consistent quotations
+ "RSE", # Errors on pytest raises.
"RET", # Simpler logic after return, raise, continue or break
- "UP018", "C408", # Convert type calls to literals. The latest pylint enforces this, but ruff has auto-fixes.
-]
-extend-select = [
- # These sets are still frequently getting new rules.
- # Therefore, they're getting frozen with the current rules so we can
- # upgrade ruff without breaking linting.
- # Pyupgrade: https://github.com/charliermarsh/ruff#pyupgrade-up
- "UP00", "UP01", "UP02", "UP030", "UP032", "UP033",
- # "UP034", # Very new, not yet enabled in ruff 0.0.227
- # Annotations: https://github.com/charliermarsh/ruff#flake8-annotations-ann
- "ANN0", # Type annotations for arguments other than `self` and `cls`
- "ANN2", # Return type annotations
- # flake8-bandit: security testing. https://github.com/charliermarsh/ruff#flake8-bandit-s
- # https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing
- "S101", "S102", # assert or exec
- "S103", "S108", # File permissions and tempfiles - use #noqa to silence when appropriate.
- "S104", # Network binds
- "S105", "S106", "S107", # Hardcoded passwords
- "S113", # Requests calls without timeouts
- "S506", # Unsafe YAML load
- "S508", "S509", # Insecure SNMP
- "S701", # jinja2 templates without autoescape
- "B0", # Common mistakes and typos.
+ "SLF", # Prevent accessing private class members.
+ "SIM", # Code simplification
+ "TID", # Tidy imports
+ # The team have chosen to only use type-checking blocks when necessary to prevent circular imports.
+ # As such, the only enabled type-checking checks are those that warn of an import that needs to be
+ # removed from a type-checking block.
+ "TCH004", # Remove imports from type-checking guard blocks if used at runtime
+ "TCH005", # Delete empty type-checking blocks
+ "ARG", # Unused arguments
+ "PTH", # Migrate to pathlib
+ "FIX", # All TODOs, FIXMEs, etc. should be turned into issues instead.
+ "ERA", # Don't check in commented out code
+ "PGH", # Pygrep hooks
+ "PL", # Pylint
+ "TRY", # Cleaner try/except,
+ "FLY", # Detect things that would be better as f-strings.
+ "PERF", # Catch things that can slow down the application like unnecessary casts to list.
"RUF001", "RUF002", "RUF003", # Ambiguous unicode characters
- "B026", # Keyword arguments must come after starred arguments
"RUF005", # Encourages unpacking rather than concatenation
- "RUF100", # #noqa directive that doesn't flag anything.
+ "RUF008", # Do not use mutable default values for dataclass attributes
+ "RUF011", # Don't use static keys in dict comprehensions.
+ "RUF013", # Prohibit implicit Optionals (PEP 484)
+ "RUF100", # #noqa directive that doesn't flag anything
+ "RUF200", # If ruff fails to parse pyproject.toml...
]
ignore = [
+ "ANN10", # Type annotations for `self` and `cls`
#"E203", # Whitespace before ":" -- Commented because ruff doesn't currently check E203
"E501", # Line too long (reason: black will automatically fix this for us)
"D105", # Missing docstring in magic method (reason: magic methods already have definitions)
@@ -154,18 +266,71 @@ ignore = [
"D213", # Multi-line docstring summary should start at the second line (reason: pep257 default)
"D215", # Section underline is over-indented (reason: pep257 default)
"A003", # Class attribute shadowing built-in (reason: Class attributes don't often get bare references)
+ "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements
+ # (reason: this creates long lines that get wrapped and reduces readability)
+ # Ignored due to common usage in current code
+ "TRY003", # Avoid specifying long messages outside the exception class
+ "COM812", # Allow trailing comma missing
+
+ # Ignored for now
+ "PGH003",
+ "PT004",
+ "PTH123",
+ "INP001",
+ "PTH118",
+ "PLR0913",
+ "PT007",
+ "PT012",
+ "ARG002",
+ "PTH110",
+ "ERA001",
+ "S101",
+ "FIX002",
+ "PYI024",
+ "PTH115",
+ "PTH207",
+ "PLR2004",
+ "ANN001",
+ "S108",
+ "FBT001",
+ "FBT002",
+ "B007",
+ "B026",
+ "PT011",
+ "PTH109",
+ "PTH116",
+ "PERF203",
+ "PERF401",
+ "E741",
+ "PLW290",
+ "TRY004",
+ "TRY200",
]
+[tool.ruff.flake8-annotations]
+allow-star-arg-any = true
+
+[tool.ruf.lint.pylint]
+max-args = 8
+
+[tool.ruff.pep8-naming]
+# Allow Pydantic's `@validator` decorator to trigger class method treatment.
+classmethod-decorators = ["pydantic.validator", "pydantic.root_validator"]
+
[tool.ruff.per-file-ignores]
"tests/**.py" = [ # Some things we want for the moin project are unnecessary in tests.
"D", # Ignore docstring rules in tests
- "ANN", # Ignore type annotations in tests
+ "ANN", # Ignore type annotations in tests
+ "ARG", # Allow unused arguments in tests (e.g. for fake functions/methods/classes)
"S101", # Allow assertions in tests
"S103", # Allow `os.chmod` setting a permissive mask `0o555` on file or directory
"S108", # Allow Probable insecure usage of temporary file or directory
- "PLR0913", # Allow many arguments for test functions
+ "PLR0913", # Allow many arguments for test functions (useful if we need many fixtures)
+ "PLR2004", # Allow magic values in tests
+ "SLF", # Allow accessing private members from tests.
]
-# isort leaves init files alone by default, this makes ruff ignore them too.
-"__init__.py" = ["I001"]
-
+"__init__.py" = [
+ "I001", # isort leaves init files alone by default, this makes ruff ignore them too.
+ "F401", # Allows unused imports in __init__ files.
+]
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index c3245bca4..000000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,136 +0,0 @@
-alabaster==0.7.13
-astroid==2.15.8
-autoflake==1.7.8
-Babel==2.13.1
-beautifulsoup4==4.12.2
-black==23.10.1
-bracex==2.4
-cachetools==5.3.2
-certifi==2023.7.22
-cffi==1.16.0
-chardet==5.2.0
-charset-normalizer==3.3.1
-click==8.1.7
-codespell==2.2.6
-colorama==0.4.6
-coverage==7.3.2
-craft-application==1.1.0
-craft-archives==1.1.3
-craft-cli==2.5.0
-craft-parts==1.26.0
-craft-providers==1.20.1
-cryptography==41.0.5
-Deprecated==1.2.14
-dill==0.3.7
-distlib==0.3.7
-distro==1.8.0
-docutils==0.18.1
-exceptiongroup==1.1.3
-filelock==3.12.4
-flake8==4.0.1
-furo==2023.9.10
-html5lib==1.1
-httplib2==0.22.0
-idna==3.4
-imagesize==1.4.1
-importlib-metadata==6.8.0
-iniconfig==2.0.0
-isort==5.12.0
-jaraco.classes==3.3.0
-jeepney==0.8.0
-Jinja2==3.1.2
-jsonpointer==2.4
-keyring==24.2.0
-launchpadlib==1.11.0
-lazr.restfulclient==0.14.5
-lazr.uri==1.0.6
-lazy-object-proxy==1.9.0
-livereload==2.6.3
-lxml==4.9.3
-Markdown==3.5
-markdown-it-py==3.0.0
-MarkupSafe==2.1.3
-mccabe==0.6.1
-mdurl==0.1.2
-more-itertools==10.1.0
-mypy==1.6.1
-mypy-extensions==1.0.0
-nh3==0.2.14
-oauthlib==3.2.2
-overrides==7.4.0
-packaging==23.2
-pathspec==0.11.2
-pkginfo==1.9.6
-platformdirs==3.11.0
-pluggy==1.3.0
-polib==1.2.0
-pycodestyle==2.8.0
-pycparser==2.21
-pydantic==1.10.13
-pydantic-yaml==0.11.2
-pydocstyle==6.3.0
-pyflakes==2.4.0
-Pygments==2.16.1
-pylint==2.17.7
-pylint-fixme-info==1.0.3
-pylint-pytest==1.1.3
-pyparsing==3.1.1
-pyproject-api==1.6.1
-pyspelling==2.9
-pytest==7.4.3
-pytest-check==2.2.2
-pytest-mock==3.12.0
-pytest-subprocess==1.5.0
-pyxdg==0.28
-PyYAML==6.0.1
-readme-renderer==42.0
-regex==2023.10.3
-requests==2.31.0
-requests-toolbelt==1.0.0
-requests-unixsocket==0.3.0
-rfc3986==2.0.0
-rich==13.6.0
-ruff==0.1.6
-SecretStorage==3.3.3
-six==1.16.0
-snowballstemmer==2.2.0
-soupsieve==2.5
-spdx==2.5.1
-spdx-lookup==0.3.3
-Sphinx==6.2.1
-sphinx-autobuild==2021.3.14
-sphinx-autodoc-typehints==1.23.0
-sphinx-basic-ng==1.0.0b2
-sphinx-copybutton==0.5.2
-sphinx-jsonschema==1.19.1
-sphinx-lint==0.8.1
-sphinx-pydantic==0.1.1
-sphinx-rtd-theme==1.3.0
-sphinx_design==0.5.0
-sphinxcontrib-applehelp==1.0.7
-sphinxcontrib-devhelp==1.0.5
-sphinxcontrib-htmlhelp==2.0.4
-sphinxcontrib-jquery==4.1
-sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-qthelp==1.0.6
-sphinxcontrib-serializinghtml==1.1.9
-tabulate==0.9.0
-tomli==2.0.1
-tomlkit==0.12.1
-tornado==6.3.3
-tox==4.11.3
-twine==4.0.2
-types-Deprecated==1.2.9.3
-types-PyYAML==6.0.12.12
-types-requests==2.31.0.6
-types-setuptools==68.2.0.0
-types-tabulate==0.9.0.3
-types-urllib3==1.26.25.14
-typing_extensions==4.8.0
-urllib3==1.26.18
-virtualenv==20.24.6
-wadllib==1.3.6
-wcmatch==8.5
-webencodings==0.5.1
-wrapt==1.15.0
-zipp==3.17.0
diff --git a/requirements-doc.txt b/requirements-doc.txt
deleted file mode 100644
index edc201dca..000000000
--- a/requirements-doc.txt
+++ /dev/null
@@ -1,78 +0,0 @@
-alabaster==0.7.13
-Babel==2.13.1
-beautifulsoup4==4.12.2
-bracex==2.4
-certifi==2023.7.22
-charset-normalizer==3.3.1
-colorama==0.4.6
-craft-application==1.1.0
-craft-archives==1.1.3
-craft-cli==2.5.0
-craft-parts==1.26.0
-craft-providers==1.20.1
-Deprecated==1.2.14
-distro==1.8.0
-docutils==0.18.1
-furo==2023.9.10
-html5lib==1.1
-httplib2==0.22.0
-idna==3.4
-imagesize==1.4.1
-importlib-metadata==6.8.0
-Jinja2==3.1.2
-jsonpointer==2.4
-launchpadlib==1.11.0
-lazr.restfulclient==0.14.5
-lazr.uri==1.0.6
-livereload==2.6.3
-lxml==4.9.3
-Markdown==3.5
-MarkupSafe==2.1.3
-oauthlib==3.2.2
-overrides==7.4.0
-packaging==23.2
-platformdirs==3.11.0
-polib==1.2.0
-pydantic==1.10.13
-pydantic-yaml==0.11.2
-Pygments==2.16.1
-pyparsing==3.1.1
-pyspelling==2.9
-pyxdg==0.28
-PyYAML==6.0.1
-regex==2023.10.3
-requests==2.31.0
-requests-unixsocket==0.3.0
-six==1.16.0
-snowballstemmer==2.2.0
-soupsieve==2.5
-spdx==2.5.1
-spdx-lookup==0.3.3
-Sphinx==6.2.1
-sphinx-autobuild==2021.3.14
-sphinx-autodoc-typehints==1.23.0
-sphinx-basic-ng==1.0.0b2
-sphinx-copybutton==0.5.2
-sphinx-jsonschema==1.19.1
-sphinx-lint==0.8.1
-sphinx-pydantic==0.1.1
-sphinx-rtd-theme==1.3.0
-sphinx_design==0.5.0
-sphinxcontrib-applehelp==1.0.7
-sphinxcontrib-devhelp==1.0.5
-sphinxcontrib-htmlhelp==2.0.4
-sphinxcontrib-jquery==4.1
-sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-qthelp==1.0.6
-sphinxcontrib-serializinghtml==1.1.9
-tabulate==0.9.0
-tornado==6.3.3
-types-Deprecated==1.2.9.3
-types-PyYAML==6.0.12.12
-typing_extensions==4.8.0
-urllib3==1.26.18
-wadllib==1.3.6
-wcmatch==8.5
-webencodings==0.5.1
-wrapt==1.15.0
-zipp==3.17.0
diff --git a/requirements-jammy.txt b/requirements-jammy.txt
deleted file mode 100644
index 13c377195..000000000
--- a/requirements-jammy.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.4.0ubuntu1/python-apt_2.4.0ubuntu1.tar.xz; sys_platform == 'linux'
diff --git a/requirements.txt b/requirements.txt
index 2d108a5e4..981b4ffe0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,4 +34,4 @@ typing_extensions==4.8.0
urllib3==1.26.18
wadllib==1.3.6
wrapt==1.15.0
-zipp==3.17.0
+zipp==3.17.0
\ No newline at end of file
diff --git a/rockcraft/application.py b/rockcraft/application.py
index f724f7171..d933a0022 100644
--- a/rockcraft/application.py
+++ b/rockcraft/application.py
@@ -16,12 +16,10 @@
"""Main Rockcraft Application."""
-from __future__ import annotations
-
from typing import Any
from craft_application import Application, AppMetadata, util
-from overrides import override
+from overrides import override # type: ignore[reportUnknownVariableType]
from rockcraft import models
from rockcraft.models import project
diff --git a/rockcraft/architectures.py b/rockcraft/architectures.py
index 118fc6cc1..2abc00753 100644
--- a/rockcraft/architectures.py
+++ b/rockcraft/architectures.py
@@ -15,8 +15,6 @@
# along with this program. If not, see .
"""Architecture definitions and conversions for Debian and Go/Docker."""
-from __future__ import annotations
-
import dataclasses
diff --git a/rockcraft/cli.py b/rockcraft/cli.py
index d8976d058..f3cbb1f73 100644
--- a/rockcraft/cli.py
+++ b/rockcraft/cli.py
@@ -17,12 +17,16 @@
"""Command-line application entry point."""
import logging
+from typing import TYPE_CHECKING
from rockcraft import plugins
from . import commands
from .services import RockcraftServiceFactory
+if TYPE_CHECKING:
+ from .application import Rockcraft
+
def run() -> int:
"""Command-line interface entrypoint."""
@@ -36,10 +40,10 @@ def run() -> int:
app = _create_app()
- return app.run()
+ return app.run() # type: ignore[no-any-return]
-def _create_app():
+def _create_app() -> "Rockcraft":
# pylint: disable=import-outside-toplevel
# Import these here so that the script that generates the docs for the
# commands doesn't need to know *too much* of the application.
diff --git a/rockcraft/commands/extensions.py b/rockcraft/commands/extensions.py
index c2c93142c..9870bde96 100644
--- a/rockcraft/commands/extensions.py
+++ b/rockcraft/commands/extensions.py
@@ -20,12 +20,11 @@
import argparse
import textwrap
from pathlib import Path
-from typing import Dict, List
import tabulate
from craft_application.commands import AppCommand
from craft_cli import emit
-from overrides import overrides
+from overrides import overrides # type: ignore[reportUnknownVariableType]
from pydantic import BaseModel
from rockcraft import extensions
@@ -36,9 +35,9 @@ class ExtensionModel(BaseModel):
"""Extension model for presentation."""
name: str
- bases: List[str]
+ bases: list[str]
- def marshal(self) -> Dict[str, str]:
+ def marshal(self) -> dict[str, str]:
"""Marshal model into a dictionary for presentation."""
return {
"Extension name": self.name,
@@ -58,9 +57,9 @@ class ListExtensionsCommand(AppCommand, abc.ABC):
)
@overrides
- def run(self, parsed_args: argparse.Namespace):
+ def run(self, parsed_args: argparse.Namespace) -> None:
"""Print the list of available extensions and their bases."""
- extension_presentation: Dict[str, ExtensionModel] = {}
+ extension_presentation: dict[str, ExtensionModel] = {}
for extension_name in extensions.registry.get_extension_names():
extension_class = extensions.registry.get_extension_class(extension_name)
@@ -96,7 +95,7 @@ class ExpandExtensionsCommand(AppCommand, abc.ABC):
)
@overrides
- def run(self, parsed_args: argparse.Namespace):
+ def run(self, parsed_args: argparse.Namespace) -> None:
"""Print the project's specification with the extensions expanded."""
project = Project.unmarshal(load_project(Path("rockcraft.yaml")))
diff --git a/rockcraft/commands/init.py b/rockcraft/commands/init.py
index 08077fe09..903bfef28 100644
--- a/rockcraft/commands/init.py
+++ b/rockcraft/commands/init.py
@@ -21,7 +21,7 @@
from craft_application.commands import AppCommand
from craft_cli import emit
-from overrides import overrides
+from overrides import overrides # type: ignore[reportUnknownVariableType]
from rockcraft import errors
diff --git a/rockcraft/extensions/_utils.py b/rockcraft/extensions/_utils.py
index ac9a28e7a..45291d21e 100644
--- a/rockcraft/extensions/_utils.py
+++ b/rockcraft/extensions/_utils.py
@@ -18,13 +18,13 @@
import copy
from pathlib import Path
-from typing import Any, Dict, List, Set, cast
+from typing import Any, cast
from .extension import Extension
from .registry import get_extension_class
-def apply_extensions(project_root: Path, yaml_data: Dict[str, Any]) -> Dict[str, Any]:
+def apply_extensions(project_root: Path, yaml_data: dict[str, Any]) -> dict[str, Any]:
"""Apply all extensions.
:param dict yaml_data: Loaded, unprocessed rockcraft.yaml
@@ -32,7 +32,7 @@ def apply_extensions(project_root: Path, yaml_data: Dict[str, Any]) -> Dict[str,
"""
# Don't modify the dict passed in
yaml_data = copy.deepcopy(yaml_data)
- declared_extensions: List[str] = cast(List[str], yaml_data.get("extensions", []))
+ declared_extensions: list[str] = cast(list[str], yaml_data.get("extensions", []))
if not declared_extensions:
return yaml_data
@@ -50,7 +50,7 @@ def apply_extensions(project_root: Path, yaml_data: Dict[str, Any]) -> Dict[str,
def _apply_extension(
- yaml_data: Dict[str, Any],
+ yaml_data: dict[str, Any],
extension: Extension,
) -> None:
# Apply the root components of the extension (if any)
@@ -65,8 +65,8 @@ def _apply_extension(
if "parts" not in yaml_data:
yaml_data["parts"] = {}
- parts = yaml_data["parts"]
- for _, part_definition in parts.items():
+ parts: dict[str, Any] = yaml_data["parts"]
+ for part_definition in parts.values():
for property_name, property_value in part_extension.items():
part_definition[property_name] = _apply_extension_property(
part_definition.get(property_name), property_value
@@ -79,7 +79,10 @@ def _apply_extension(
parts[part_name] = parts_snippet[part_name]
-def _apply_extension_property(existing_property: Any, extension_property: Any) -> Any:
+def _apply_extension_property(
+ existing_property: list[Any] | dict[str, Any] | None,
+ extension_property: list[Any] | dict[str, Any],
+) -> list[Any] | dict[str, Any] | None:
if existing_property:
# If the property is not scalar, merge them
if isinstance(existing_property, list) and isinstance(extension_property, list):
@@ -102,10 +105,10 @@ def _apply_extension_property(existing_property: Any, extension_property: Any) -
return extension_property
-def _remove_list_duplicates(seq: List[str]) -> List[str]:
+def _remove_list_duplicates(seq: list[str]) -> list[str]:
"""De-dupe string list maintaining ordering."""
- seen: Set[str] = set()
- deduped: List[str] = []
+ seen: set[str] = set()
+ deduped: list[str] = []
for item in seq:
if item not in seen:
diff --git a/rockcraft/extensions/extension.py b/rockcraft/extensions/extension.py
index 96de34999..efe87b63a 100644
--- a/rockcraft/extensions/extension.py
+++ b/rockcraft/extensions/extension.py
@@ -19,8 +19,9 @@
import abc
import os
import sys
+from collections.abc import Sequence
from pathlib import Path
-from typing import Any, Dict, Optional, Sequence, Tuple, final
+from typing import Any, final
from craft_cli import emit
@@ -41,7 +42,7 @@ def __init__(
self,
*,
project_root: Path,
- yaml_data: Dict[str, Any],
+ yaml_data: dict[str, Any],
) -> None:
"""Create a new Extension."""
self.project_root = project_root
@@ -49,28 +50,28 @@ def __init__(
@staticmethod
@abc.abstractmethod
- def get_supported_bases() -> Tuple[str, ...]:
+ def get_supported_bases() -> tuple[str, ...]:
"""Return a tuple of supported bases."""
@staticmethod
@abc.abstractmethod
- def is_experimental(base: Optional[str]) -> bool:
+ def is_experimental(base: str | None) -> bool:
"""Return whether or not this extension is unstable for given base."""
@abc.abstractmethod
- def get_root_snippet(self) -> Dict[str, Any]:
+ def get_root_snippet(self) -> dict[str, Any]:
"""Return the root snippet to apply."""
@abc.abstractmethod
- def get_part_snippet(self) -> Dict[str, Any]:
+ def get_part_snippet(self) -> dict[str, Any]:
"""Return the part snippet to apply to existing parts."""
@abc.abstractmethod
- def get_parts_snippet(self) -> Dict[str, Any]:
+ def get_parts_snippet(self) -> dict[str, Any]:
"""Return the parts to add to parts."""
@final
- def validate(self, extension_name: str):
+ def validate(self, extension_name: str) -> None:
"""Validate that the extension can be used with the current project.
:param extension_name: the name of the extension being parsed.
diff --git a/rockcraft/extensions/registry.py b/rockcraft/extensions/registry.py
index d48b59744..98c7f4563 100644
--- a/rockcraft/extensions/registry.py
+++ b/rockcraft/extensions/registry.py
@@ -16,19 +16,19 @@
"""Extension registry."""
-from typing import TYPE_CHECKING, Dict, List, Type
+from typing import TYPE_CHECKING
from rockcraft import errors
if TYPE_CHECKING:
from .extension import Extension
- ExtensionType = Type[Extension]
+ ExtensionType = type[Extension]
-_EXTENSIONS: Dict[str, "ExtensionType"] = {}
+_EXTENSIONS: dict[str, "ExtensionType"] = {}
-def get_extension_names() -> List[str]:
+def get_extension_names() -> list[str]:
"""Obtain a extension class given the name.
:param name: The extension name.
diff --git a/rockcraft/layers.py b/rockcraft/layers.py
index b8a2766f0..2c6fae7d9 100644
--- a/rockcraft/layers.py
+++ b/rockcraft/layers.py
@@ -19,7 +19,6 @@
import tarfile
from collections import defaultdict
from pathlib import Path
-from typing import DefaultDict, Dict, List, Optional, Set, Tuple
from craft_cli import emit
from craft_parts.executor.collisions import paths_collide
@@ -32,7 +31,7 @@
def archive_layer(
new_layer_dir: Path,
temp_tar_file: Path,
- base_layer_dir: Optional[Path] = None,
+ base_layer_dir: Path | None = None,
) -> None:
"""Prepare new OCI layer by archiving its content into tar file.
@@ -55,7 +54,7 @@ def archive_layer(
tar_file.add(filepath, arcname=arcname, recursive=False)
-def prune_prime_files(prime_dir: Path, files: Set[str], base_layer_dir: Path) -> None:
+def prune_prime_files(prime_dir: Path, files: set[str], base_layer_dir: Path) -> None:
"""Remove (prune) files in a prime directory if they exist in the base layer.
Given a set of filenames ``files``, this function will remove (prune) all those
@@ -87,8 +86,8 @@ def prune_prime_files(prime_dir: Path, files: Set[str], base_layer_dir: Path) ->
def _gather_layer_paths(
- new_layer_dir: Path, base_layer_dir: Optional[Path] = None
-) -> Dict[str, List[Path]]:
+ new_layer_dir: Path, base_layer_dir: Path | None = None
+) -> dict[str, list[Path]]:
"""Map paths in ``new_layer_dir`` to names in a layer file.
See ``_archive_layer()`` for the parameters.
@@ -132,7 +131,7 @@ def get_target_path(self, path: Path) -> Path:
return path
layer_linker = LayerLinker()
- result: DefaultDict[str, List[Path]] = defaultdict(list)
+ result: defaultdict[str, list[Path]] = defaultdict(list)
for dirpath, subdirs, filenames in os.walk(new_layer_dir):
# Sort `subdirs` in-place, to ensure that `os.walk()` iterates on
# them in sorted order.
@@ -180,7 +179,7 @@ def get_target_path(self, path: Path) -> Path:
return result
-def _merge_layer_paths(candidate_paths: Dict[str, List[Path]]) -> Dict[str, Path]:
+def _merge_layer_paths(candidate_paths: dict[str, list[Path]]) -> dict[str, Path]:
"""Merge ``candidate_paths`` into a single path per name.
This function handles the case where multiple paths refer to the same name
@@ -192,7 +191,7 @@ def _merge_layer_paths(candidate_paths: Dict[str, List[Path]]) -> Dict[str, Path
A dict where the values are Paths and the keys are the names those paths
correspond to in the new layer.
"""
- result: Dict[str, Path] = {}
+ result: dict[str, Path] = {}
for name, paths in candidate_paths.items():
if len(paths) == 1:
@@ -224,8 +223,8 @@ def _merge_layer_paths(candidate_paths: Dict[str, List[Path]]) -> Dict[str, Path
def _symlink_target_in_base_layer(
- relative_path: Path, base_layer_dir: Optional[Path]
-) -> Optional[Path]:
+ relative_path: Path, base_layer_dir: Path | None
+) -> Path | None:
"""If `relative_path` is a dir symlink in `base_layer_dir`, return its 'target'.
This function checks if `relative_path` exists in the base `base_layer_dir` as
@@ -246,7 +245,7 @@ def _symlink_target_in_base_layer(
return None
-def _all_compatible_directories(paths: List[Path]) -> bool:
+def _all_compatible_directories(paths: list[Path]) -> bool:
"""Whether ``paths`` contains only directories with the same ownership and permissions."""
if not all(p.is_dir() for p in paths):
return False
@@ -254,7 +253,7 @@ def _all_compatible_directories(paths: List[Path]) -> bool:
if len(paths) < 2:
return True
- def stat_props(stat: os.stat_result) -> Tuple[int, int, int]:
+ def stat_props(stat: os.stat_result) -> tuple[int, int, int]:
return stat.st_uid, stat.st_gid, stat.st_mode
first_stat = stat_props(paths[0].stat())
@@ -263,17 +262,15 @@ def stat_props(stat: os.stat_result) -> Tuple[int, int, int]:
other_stat = stat_props(other_path.stat())
if first_stat != other_stat:
emit.debug(
- (
- f"Path attributes differ for '{paths[0]}' and '{other_path}': "
- f"{first_stat} vs {other_stat}"
- )
+ f"Path attributes differ for '{paths[0]}' and '{other_path}': "
+ f"{first_stat} vs {other_stat}"
)
return False
return True
-def _all_compatible_files(paths: List[Path]) -> bool:
+def _all_compatible_files(paths: list[Path]) -> bool:
"""Whether ``paths`` contains only files with the same attributes and contents."""
if not all(p.is_file() for p in paths):
return False
diff --git a/rockcraft/models/project.py b/rockcraft/models/project.py
index 42a2f1b9b..1d6db58a3 100644
--- a/rockcraft/models/project.py
+++ b/rockcraft/models/project.py
@@ -17,18 +17,12 @@
"""Project definition and helpers."""
import re
import shlex
+from collections.abc import Callable, Mapping, Sequence
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
- Dict,
- List,
Literal,
- Mapping,
- Optional,
- Sequence,
- Tuple,
- Union,
cast,
)
@@ -38,7 +32,7 @@
import yaml
from craft_application.models import BuildInfo
from craft_application.models import Project as BaseProject
-from craft_archives import repo
+from craft_archives import repo # type: ignore[import-untyped]
from craft_providers import bases
from pydantic_yaml import YamlModelMixin
@@ -52,26 +46,32 @@
if TYPE_CHECKING: # pragma: no cover
from pydantic.error_wrappers import ErrorDict
+# pyright workaround
+if TYPE_CHECKING:
+ _RunUser = str | None
+else:
+ _RunUser = Literal[tuple(SUPPORTED_GLOBAL_USERNAMES)] | None
+
class Platform(pydantic.BaseModel):
"""Rockcraft project platform definition."""
- build_on: Optional[ # type: ignore
- pydantic.conlist(str, unique_items=True, min_items=1) # type: ignore
- ]
- build_for: Optional[ # type: ignore
- pydantic.conlist(str, unique_items=True, min_items=1) # type: ignore
- ]
+ build_on: pydantic.conlist(str, unique_items=True, min_items=1) | None # type: ignore[valid-type]
+ build_for: pydantic.conlist( # type: ignore[valid-type]
+ str, unique_items=True, min_items=1
+ ) | None
class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
allow_population_by_field_name = True
- alias_generator = lambda s: s.replace("_", "-") # noqa: E731
+ alias_generator: Callable[[str], str] = lambda s: s.replace( # noqa: E731
+ "_", "-"
+ )
@pydantic.validator("build_for", pre=True)
@classmethod
- def _vectorise_build_for(cls, val: Union[str, List[str]]) -> List[str]:
+ def _vectorise_build_for(cls, val: str | list[str]) -> list[str]:
"""Vectorise target architecture if needed."""
if isinstance(val, str):
val = [val]
@@ -81,8 +81,8 @@ def _vectorise_build_for(cls, val: Union[str, List[str]]) -> List[str]:
@classmethod
def _validate_platform_set(cls, values: Mapping[str, Any]) -> Mapping[str, Any]:
"""Validate the build_on build_for combination."""
- build_for = values["build_for"] if values.get("build_for") else []
- build_on = values["build_on"] if values.get("build_on") else []
+ build_for: list[str] = values["build_for"] if values.get("build_for") else []
+ build_on: list[str] = values["build_on"] if values.get("build_on") else []
# We can only build for 1 arch at the moment
if len(build_for) > 1:
@@ -134,20 +134,20 @@ class Project(YamlModelMixin, BaseProject):
name: NameStr # type: ignore
# summary is Optional[str] in BaseProject
summary: str # type: ignore
- description: str
+ description: str # type: ignore[reportIncompatibleVariableOverride]
rock_license: str = pydantic.Field(alias="license")
- platforms: Dict[str, Any]
+ platforms: dict[str, Any]
base: Literal["bare", "ubuntu@20.04", "ubuntu@22.04"]
- build_base: Optional[Literal["ubuntu@20.04", "ubuntu@22.04"]]
- environment: Optional[Dict[str, str]]
- run_user: Optional[Literal[tuple(SUPPORTED_GLOBAL_USERNAMES)]] # type: ignore
- services: Optional[Dict[str, Service]]
- checks: Optional[Dict[str, Check]]
- entrypoint_service: Optional[str]
+ build_base: Literal["ubuntu@20.04", "ubuntu@22.04"] | None
+ environment: dict[str, str] | None
+ run_user: _RunUser
+ services: dict[str, Service] | None
+ checks: dict[str, Check] | None
+ entrypoint_service: str | None
- package_repositories: Optional[List[Dict[str, Any]]]
+ package_repositories: list[dict[str, Any]] | None
- parts: Dict[str, Any]
+ parts: dict[str, Any]
class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
@@ -156,7 +156,9 @@ class Config: # pylint: disable=too-few-public-methods
extra = "forbid"
allow_mutation = False
allow_population_by_field_name = True
- alias_generator = lambda s: s.replace("_", "-") # noqa: E731
+ alias_generator: Callable[[str], str] = lambda s: s.replace( # noqa: E731
+ "_", "-"
+ )
error_msg_templates = {
"value_error.str.regex": INVALID_NAME_MESSAGE,
}
@@ -193,16 +195,16 @@ def _validate_license(cls, rock_license: str) -> str:
# This is the license name we use on our stores.
return rock_license
- lic: Optional[spdx_lookup.License] = spdx_lookup.by_id(rock_license)
+ lic: spdx_lookup.License | None = spdx_lookup.by_id(rock_license) # type: ignore[reportUnknownMemberType]
if lic is None:
raise ProjectValidationError(
f"License {rock_license} not valid. It must be valid and in SPDX format."
)
- return lic.id
+ return str(lic.id) # type: ignore[reportUnknownMemberType]
@pydantic.validator("title", always=True)
@classmethod
- def _validate_title(cls, title: Optional[str], values: Mapping[str, Any]) -> str:
+ def _validate_title(cls, title: str | None, values: Mapping[str, Any]) -> str:
"""If title is not provided, it defaults to the provided ROCK name."""
if not title:
title = values.get("name", "")
@@ -210,7 +212,9 @@ def _validate_title(cls, title: Optional[str], values: Mapping[str, Any]) -> str
@pydantic.validator("build_base", always=True)
@classmethod
- def _validate_build_base(cls, build_base: Optional[str], values: Any) -> str:
+ def _validate_build_base(
+ cls, build_base: str | None, values: dict[str, Any]
+ ) -> str:
"""Build-base defaults to the base value if not specified.
:raises ProjectValidationError: If base validation fails.
@@ -226,20 +230,16 @@ def _validate_build_base(cls, build_base: Optional[str], values: Any) -> str:
@pydantic.validator("base", pre=True)
@classmethod
- def _validate_deprecated_base(cls, base_value: Optional[str]) -> Optional[str]:
+ def _validate_deprecated_base(cls, base_value: str | None) -> str | None:
return cls._check_deprecated_base(base_value, "base")
@pydantic.validator("build_base", pre=True)
@classmethod
- def _validate_deprecated_build_base(
- cls, base_value: Optional[str]
- ) -> Optional[str]:
+ def _validate_deprecated_build_base(cls, base_value: str | None) -> str | None:
return cls._check_deprecated_base(base_value, "build_base")
@staticmethod
- def _check_deprecated_base(
- base_value: Optional[str], field_name: str
- ) -> Optional[str]:
+ def _check_deprecated_base(base_value: str | None, field_name: str) -> str | None:
if base_value in DEPRECATED_COLON_BASES:
at_value = base_value.replace(":", "@")
message = (
@@ -253,10 +253,12 @@ def _check_deprecated_base(
@pydantic.validator("platforms")
@classmethod
- def _validate_all_platforms(cls, platforms: Dict[str, Any]) -> Dict[str, Any]:
+ def _validate_all_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]:
"""Make sure all provided platforms are tangible and sane."""
for platform_label in platforms:
- platform = platforms[platform_label] if platforms[platform_label] else {}
+ platform: dict[str, Any] = (
+ platforms[platform_label] if platforms[platform_label] else {}
+ )
error_prefix = f"Error for platform entry '{platform_label}'"
# Make sure the provided platform_set is valid
@@ -313,14 +315,16 @@ def _validate_all_platforms(cls, platforms: Dict[str, Any]) -> Dict[str, Any]:
@pydantic.validator("parts", each_item=True)
@classmethod
- def _validate_parts(cls, item: Dict[str, Any]) -> Dict[str, Any]:
+ def _validate_parts(cls, item: dict[str, Any]) -> dict[str, Any]:
"""Verify each part (craft-parts will re-validate this)."""
validate_part(item)
return item
@pydantic.validator("parts", each_item=True)
@classmethod
- def _validate_base_and_overlay(cls, item: Dict[str, Any], values) -> Dict[str, Any]:
+ def _validate_base_and_overlay(
+ cls, item: dict[str, Any], values: dict[str, Any]
+ ) -> dict[str, Any]:
"""Projects with "bare" bases cannot use overlays."""
if values.get("base") == "bare" and part_has_overlay(item):
raise ProjectValidationError(
@@ -331,23 +335,23 @@ def _validate_base_and_overlay(cls, item: Dict[str, Any], values) -> Dict[str, A
@pydantic.validator("entrypoint_service")
@classmethod
def _validate_entrypoint_service(
- cls, entrypoint_service: Optional[str], values: Any
- ) -> Optional[str]:
+ cls, entrypoint_service: str | None, values: dict[str, Any]
+ ) -> str | None:
"""Verify that the entrypoint_service exists in the services dict."""
craft_cli.emit.message(
"Warning: defining an entrypoint-service will result in a rock with "
- + "an atypical OCI Entrypoint. While that might be acceptable for "
- + "testing and personal use, it shall require prior approval before "
- + "submitting to a Canonical registry namespace."
+ "an atypical OCI Entrypoint. While that might be acceptable for "
+ "testing and personal use, it shall require prior approval before "
+ "submitting to a Canonical registry namespace."
)
if entrypoint_service not in values.get("services", {}):
raise ProjectValidationError(
f"The provided entrypoint-service '{entrypoint_service}' is not "
- + "a valid Pebble service."
+ "a valid Pebble service."
)
- command = values.get("services")[entrypoint_service].command
+ command = values["services"][entrypoint_service].command
command_sh_args = shlex.split(command)
# optional arg is surrounded by brackets, so check that they exist in the
# right order
@@ -355,13 +359,13 @@ def _validate_entrypoint_service(
if command_sh_args.index("[") >= command_sh_args.index("]"):
raise IndexError(
"Bad syntax for the entrypoint-service command's"
- + " additional args."
+ " additional args."
)
except ValueError as ex:
raise ProjectValidationError(
f"The Pebble service '{entrypoint_service}' has a command "
- + f"{command} without default arguments and thus cannot be used "
- + "as the entrypoint-service."
+ f"{command} without default arguments and thus cannot be used "
+ "as the entrypoint-service."
) from ex
return entrypoint_service
@@ -369,12 +373,12 @@ def _validate_entrypoint_service(
@pydantic.validator("package_repositories")
@classmethod
def _validate_package_repositories(
- cls, package_repositories: Optional[List[Dict[str, Any]]]
- ) -> List[Dict[str, Any]]:
+ cls, package_repositories: list[dict[str, Any]] | None
+ ) -> list[dict[str, Any]]:
if not package_repositories:
return []
- errors = []
+ errors: list[ErrorDict] = []
for repository in package_repositories:
try:
repo.validate_repository(repository)
@@ -390,8 +394,8 @@ def _validate_package_repositories(
@pydantic.validator("environment")
@classmethod
def _forbid_env_var_bash_interpolation(
- cls, environment: Optional[Dict[str, str]]
- ) -> Dict[str, str]:
+ cls, environment: dict[str, str] | None
+ ) -> dict[str, str]:
"""Variable interpolation isn't yet supported, so forbid attempts to do it."""
if not environment:
return {}
@@ -412,14 +416,14 @@ def _forbid_env_var_bash_interpolation(
def to_yaml(self) -> str:
"""Dump this project as a YAML string."""
- def _repr_str(dumper, data):
+ def _repr_str(dumper: yaml.SafeDumper, data: str) -> yaml.ScalarNode:
"""Multi-line string representer for the YAML dumper."""
if "\n" in data:
- return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
- return dumper.represent_scalar("tag:yaml.org,2002:str", data)
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") # type: ignore[reportUnknownMemberType]
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data) # type: ignore[reportUnknownMemberType]
yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper)
- return super().yaml(
+ return super().yaml( # type: ignore[reportUnknownMemberType]
by_alias=True,
exclude_none=True,
allow_unicode=True,
@@ -428,11 +432,8 @@ def _repr_str(dumper, data):
)
@classmethod
- def unmarshal(cls, data: Dict[str, Any]) -> "Project":
+ def unmarshal(cls, data: dict[str, Any]) -> "Project":
"""Overridden to raise ProjectValidationError() for Pydantic errors."""
- if not isinstance(data, dict):
- raise TypeError("project data is not a dictionary")
-
try:
project = super().unmarshal(data)
except pydantic.ValidationError as err:
@@ -442,7 +443,7 @@ def unmarshal(cls, data: Dict[str, Any]) -> "Project":
def generate_metadata(
self, generation_time: str, base_digest: bytes
- ) -> Tuple[dict, dict]:
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
"""Generate the ROCK's metadata (both the OCI annotation and internal metadata.
:param generation_time: the UTC time at the time of calling this method
@@ -471,9 +472,9 @@ def generate_metadata(
return (annotations, metadata)
- def get_build_plan(self) -> List[BuildInfo]:
+ def get_build_plan(self) -> list[BuildInfo]:
"""Obtain the list of architectures and bases from the project file."""
- build_infos: List[BuildInfo] = []
+ build_infos: list[BuildInfo] = []
base = self.effective_base
for platform_entry, platform in self.platforms.items():
@@ -492,10 +493,10 @@ def get_build_plan(self) -> List[BuildInfo]:
def _format_pydantic_errors(
- errors: List["ErrorDict"],
+ errors: list["ErrorDict"],
*,
file_name: str = "rockcraft.yaml",
- base_location: Optional[str] = None,
+ base_location: str | None = None,
) -> str:
"""Format errors.
@@ -516,7 +517,7 @@ def _format_pydantic_errors(
combined = [f"Bad {file_name} content:"]
for error in errors:
if base_location:
- error_loc: List[Union[int, str]] = [base_location]
+ error_loc: list[int | str] = [base_location]
else:
error_loc = []
error_loc.extend(error["loc"])
@@ -539,36 +540,32 @@ def _format_pydantic_errors(
return "\n".join(combined)
-def _format_pydantic_error_location(loc: Sequence[Union[str, int]]) -> str:
+def _format_pydantic_error_location(loc: Sequence[str | int]) -> str:
"""Format location."""
- loc_parts = []
+ loc_parts: list[str] = []
for loc_part in loc:
if isinstance(loc_part, str):
loc_parts.append(loc_part)
- elif isinstance(loc_part, int):
+ else:
# Integer indicates an index. Go
# back and fix up previous part.
previous_part = loc_parts.pop()
previous_part += f"[{loc_part}]"
loc_parts.append(previous_part)
- else:
- raise RuntimeError(f"unhandled loc: {loc_part}")
new_loc = ".".join(loc_parts)
# Filter out internal __root__ detail.
- new_loc = new_loc.replace(".__root__", "")
- return new_loc
+ return new_loc.replace(".__root__", "")
def _format_pydantic_error_message(msg: str) -> str:
"""Format pydantic's error message field."""
# Replace shorthand "str" with "string".
- msg = msg.replace("str type expected", "string type expected")
- return msg
+ return msg.replace("str type expected", "string type expected")
-def _printable_field_location_split(location: str) -> Tuple[str, str]:
+def _printable_field_location_split(location: str) -> tuple[str, str]:
"""Return split field location.
If top-level, location is returned as unquoted "top-level".
@@ -588,7 +585,7 @@ def _printable_field_location_split(location: str) -> Tuple[str, str]:
return field_name, "top-level"
-def load_project(filename: Path) -> Dict[str, Any]:
+def load_project(filename: Path) -> dict[str, Any]:
"""Load and unmarshal the project YAML file.
:param filename: The YAML file to load.
@@ -607,12 +604,10 @@ def load_project(filename: Path) -> Dict[str, Any]:
msg = f"{msg}: {err.filename!r}."
raise ProjectLoadError(msg) from err
- yaml_data = transform_yaml(filename.parent, yaml_data)
-
- return yaml_data
+ return transform_yaml(filename.parent, yaml_data)
-def transform_yaml(project_root: Path, yaml_data: Dict[str, Any]) -> Dict[str, Any]:
+def transform_yaml(project_root: Path, yaml_data: dict[str, Any]) -> dict[str, Any]:
"""Do Rockcraft-specific transformations on a project yaml.
:param project_root: The path that contains the "rockcraft.yaml" file.
@@ -625,7 +620,7 @@ def transform_yaml(project_root: Path, yaml_data: Dict[str, Any]) -> Dict[str, A
return yaml_data
-def _add_pebble_data(yaml_data: Dict[str, Any]) -> None:
+def _add_pebble_data(yaml_data: dict[str, Any]) -> None:
"""Add pebble-specific contents to YAML-loaded data.
This function adds a special "pebble" part to a project's specification, to be
diff --git a/rockcraft/oci.py b/rockcraft/oci.py
index 4e496c833..a0df5b48e 100644
--- a/rockcraft/oci.py
+++ b/rockcraft/oci.py
@@ -27,7 +27,7 @@
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any
import yaml
from craft_cli import emit
@@ -67,7 +67,7 @@ def from_docker_registry(
*,
image_dir: Path,
arch: str,
- ) -> Tuple["Image", str]:
+ ) -> tuple["Image", str]:
"""Obtain an image from a docker registry.
The image is fetched from the registry at ``REGISTRY_URL``.
@@ -115,7 +115,7 @@ def new_oci_image(
image_name: str,
image_dir: Path,
arch: str,
- ) -> Tuple["Image", str]:
+ ) -> tuple["Image", str]:
"""Create a new OCI image out of thin air.
:param image_name: The image to initiate, in ``name@tag`` format.
@@ -193,7 +193,7 @@ def add_layer(
self,
tag: str,
new_layer_dir: Path,
- base_layer_dir: Optional[Path] = None,
+ base_layer_dir: Path | None = None,
) -> "Image":
"""Add a layer to the image.
@@ -295,13 +295,14 @@ def add_user(
emit.progress(f"Adding user {username}:{uid} with group {username}:{uid}")
self.add_layer(tag, Path(tmpfs))
- def stat(self) -> Dict[Any, Any]:
+ def stat(self) -> dict[str, Any]:
"""Obtain the image statistics, as reported by "umoci stat --json"."""
image_path = self.path / self.image_name
- output = _process_run(
+ output: bytes = _process_run(
["umoci", "stat", "--json", "--image", str(image_path)]
).stdout
- return json.loads(output)
+ result: dict[str, Any] = json.loads(output)
+ return result
@staticmethod
def digest(source_image: str) -> bytes:
@@ -352,7 +353,7 @@ def set_default_user(self, user: str) -> None:
_config_image(image_path, params)
emit.progress(f"Default user set to {user}")
- def set_entrypoint(self, entrypoint_service: Optional[str] = None) -> None:
+ def set_entrypoint(self, entrypoint_service: str | None = None) -> None:
"""Set the OCI image entrypoint. It is always Pebble."""
emit.progress("Configuring entrypoint...")
image_path = self.path / self.image_name
@@ -366,7 +367,7 @@ def set_entrypoint(self, entrypoint_service: Optional[str] = None) -> None:
_config_image(image_path, params)
emit.progress(f"Entrypoint set to {entrypoint}")
- def set_cmd(self, command: Optional[str] = None) -> None:
+ def set_cmd(self, command: str | None = None) -> None:
"""Set the OCI image CMD."""
emit.progress("Configuring CMD...")
image_path = self.path / self.image_name
@@ -379,7 +380,7 @@ def set_cmd(self, command: Optional[str] = None) -> None:
except ValueError:
emit.debug(
f"The entrypoint-service command '{command}' has no default "
- + "arguments. CMD won't be set."
+ "arguments. CMD won't be set."
)
return
for arg in opt_args:
@@ -389,8 +390,8 @@ def set_cmd(self, command: Optional[str] = None) -> None:
def set_pebble_layer(
self,
- services: Dict[str, Any],
- checks: Dict[str, Any],
+ services: dict[str, Any],
+ checks: dict[str, Any],
name: str,
tag: str,
summary: str,
@@ -408,7 +409,7 @@ def set_pebble_layer(
:param base_layer_dir: Path to the base layer's root filesystem
"""
# pylint: disable=too-many-arguments
- pebble_layer_content: Dict[str, Any] = {
+ pebble_layer_content: dict[str, Any] = {
"summary": summary,
"description": description,
}
@@ -433,7 +434,7 @@ def set_pebble_layer(
emit.progress("Writing new Pebble layer file")
self.add_layer(tag, tmpfs_path)
- def set_environment(self, env: Dict[str, str]) -> None:
+ def set_environment(self, env: dict[str, str]) -> None:
"""Set the OCI image environment.
:param env: A dictionary mapping environment variables to
@@ -441,8 +442,8 @@ def set_environment(self, env: Dict[str, str]) -> None:
"""
emit.progress("Configuring OCI environment...")
image_path = self.path / self.image_name
- params = []
- env_list = []
+ params: list[str] = []
+ env_list: list[str] = []
for name, value in env.items():
env_item = f"{name}={value}"
@@ -451,7 +452,7 @@ def set_environment(self, env: Dict[str, str]) -> None:
_config_image(image_path, params)
emit.progress(f"Environment set to {env_list}")
- def set_control_data(self, metadata: Dict[str, Any]) -> None:
+ def set_control_data(self, metadata: dict[str, Any]) -> None:
"""Create and populate the ROCK's control data folder.
:param metadata: content for the ROCK's metadata YAML file
@@ -480,7 +481,7 @@ def set_control_data(self, metadata: Dict[str, Any]) -> None:
emit.progress("Control data written")
shutil.rmtree(local_control_data_path)
- def set_annotations(self, annotations: Dict[str, Any]) -> None:
+ def set_annotations(self, annotations: dict[str, Any]) -> None:
"""Add the given annotations to the final image.
:param annotations: A dictionary with each annotation/label and its value
@@ -490,7 +491,7 @@ def set_annotations(self, annotations: Dict[str, Any]) -> None:
label_params = ["--clear=config.labels"]
annotation_params = ["--clear=manifest.annotations"]
- labels_list = []
+ labels_list: list[str] = []
for label_key, label_value in annotations.items():
label_item = f"{label_key}={label_value}"
labels_list.append(label_item)
@@ -507,7 +508,7 @@ def _copy_image(
source: str,
destination: str,
*system_params: str,
- copy_params: Optional[List[str]] = None,
+ copy_params: list[str] | None = None,
) -> None:
"""Transfer images from source to destination.
@@ -519,9 +520,7 @@ def _copy_image(
[
"skopeo",
"--insecure-policy",
- ]
- + list(system_params)
- + [
+ *list(system_params),
"copy",
*copy_extra,
source,
@@ -530,9 +529,9 @@ def _copy_image(
)
-def _config_image(image_path: Path, params: List[str]) -> None:
+def _config_image(image_path: Path, params: list[str]) -> None:
"""Configure the OCI image."""
- _process_run(["umoci", "config", "--image", str(image_path)] + params)
+ _process_run(["umoci", "config", "--image", str(image_path), *params])
def _add_layer_into_image(
@@ -551,7 +550,7 @@ def _add_layer_into_image(
str(image_path),
str(archived_content),
] + [arg_val for k, v in kwargs.items() for arg_val in [k, v]]
- _process_run(cmd + ["--history.created_by", " ".join(cmd)])
+ _process_run([*cmd, "--history.created_by", " ".join(cmd)])
def _inject_architecture_variant(image_path: Path, variant: str) -> None:
@@ -600,7 +599,7 @@ def _inject_architecture_variant(image_path: Path, variant: str) -> None:
tl_index_path.write_bytes(json.dumps(tl_index).encode("utf-8"))
-def _process_run(command: List[str], **kwargs: Any) -> subprocess.CompletedProcess:
+def _process_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[Any]:
"""Run a command and handle its output."""
if not Path(command[0]).is_absolute():
command[0] = get_snap_command_path(command[0])
@@ -613,7 +612,7 @@ def _process_run(command: List[str], **kwargs: Any) -> subprocess.CompletedProce
**kwargs,
capture_output=True,
check=True,
- universal_newlines=True,
+ text=True,
)
except subprocess.CalledProcessError as err:
msg = f"Failed to copy image: {err!s}"
diff --git a/rockcraft/parts.py b/rockcraft/parts.py
index 36ad00b16..dac212b15 100644
--- a/rockcraft/parts.py
+++ b/rockcraft/parts.py
@@ -15,12 +15,12 @@
# along with this program. If not, see .
"""Craft-parts lifecycle."""
-from typing import Any, Dict
+from typing import Any
import craft_parts
-def validate_part(data: Dict[str, Any]) -> None:
+def validate_part(data: dict[str, Any]) -> None:
"""Validate the given part data against common and plugin models.
:param data: The part data to validate.
@@ -28,6 +28,6 @@ def validate_part(data: Dict[str, Any]) -> None:
craft_parts.validate_part(data)
-def part_has_overlay(data: Dict[str, Any]) -> bool:
+def part_has_overlay(data: dict[str, Any]) -> bool:
"""Whether ``data`` declares an overlay-using part."""
return craft_parts.part_has_overlay(data)
diff --git a/rockcraft/pebble.py b/rockcraft/pebble.py
index c80bae457..feccab07a 100644
--- a/rockcraft/pebble.py
+++ b/rockcraft/pebble.py
@@ -17,8 +17,9 @@
"""Pebble metadata and configuration helpers."""
import glob
+from collections.abc import Callable, Mapping
from pathlib import Path
-from typing import Any, Dict, List, Literal, Mapping, Optional
+from typing import Any, Literal
import pydantic
import yaml
@@ -31,7 +32,7 @@ class HttpCheck(pydantic.BaseModel):
"""Lightweight schema validation for a Pebble HTTP check."""
url: pydantic.AnyHttpUrl
- headers: Optional[Dict[str, str]]
+ headers: dict[str, str] | None
class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
@@ -44,7 +45,7 @@ class TcpCheck(pydantic.BaseModel):
"""Lightweight schema validation for a Pebble TCP check."""
port: int
- host: Optional[str]
+ host: str | None
class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
@@ -57,19 +58,21 @@ class ExecCheck(pydantic.BaseModel):
"""Lightweight schema validation for a Pebble exec check."""
command: str
- service_context: Optional[str]
- environment: Optional[Dict[str, str]]
- user: Optional[str]
- user_id: Optional[int]
- group: Optional[str]
- group_id: Optional[int]
- working_dir: Optional[str]
+ service_context: str | None
+ environment: dict[str, str] | None
+ user: str | None
+ user_id: int | None
+ group: str | None
+ group_id: int | None
+ working_dir: str | None
class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
allow_population_by_field_name = True
- alias_generator = lambda s: s.replace("_", "-") # noqa: E731
+ alias_generator: Callable[[str], str] = lambda s: s.replace( # noqa: E731
+ "_", "-"
+ )
extra = "forbid"
@@ -81,13 +84,13 @@ class Check(pydantic.BaseModel):
"""
override: Literal["merge", "replace"]
- level: Optional[Literal["alive", "ready"]]
- period: Optional[str]
- timeout: Optional[str]
- threshold: Optional[int]
- http: Optional[HttpCheck]
- tcp: Optional[TcpCheck]
- exec: Optional[ExecCheck]
+ level: Literal["alive", "ready"] | None
+ period: str | None
+ timeout: str | None
+ threshold: int | None
+ http: HttpCheck | None
+ tcp: TcpCheck | None
+ exec: ExecCheck | None
@pydantic.root_validator(pre=True)
@classmethod
@@ -115,7 +118,9 @@ class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
allow_population_by_field_name = True
- alias_generator = lambda s: s.replace("_", "-") # noqa: E731
+ alias_generator: Callable[[str], str] = lambda s: s.replace( # noqa: E731
+ "_", "-"
+ )
extra = "forbid"
@@ -128,31 +133,33 @@ class Service(pydantic.BaseModel):
override: Literal["merge", "replace"]
command: str
- summary: Optional[str]
- description: Optional[str]
- startup: Optional[Literal["enabled", "disabled"]]
- after: Optional[List[str]]
- before: Optional[List[str]]
- requires: Optional[List[str]]
- environment: Optional[Dict[str, str]]
- user: Optional[str]
- user_id: Optional[int]
- group: Optional[str]
- group_id: Optional[int]
- working_dir: Optional[str]
- on_success: Optional[Literal["restart", "shutdown", "ignore"]]
- on_failure: Optional[Literal["restart", "shutdown", "ignore"]]
- on_check_failure: Optional[Dict[str, Literal["restart", "shutdown", "ignore"]]]
- backoff_delay: Optional[str]
- backoff_factor: Optional[float]
- backoff_limit: Optional[str]
- kill_delay: Optional[str]
+ summary: str | None
+ description: str | None
+ startup: Literal["enabled", "disabled"] | None
+ after: list[str] | None
+ before: list[str] | None
+ requires: list[str] | None
+ environment: dict[str, str] | None
+ user: str | None
+ user_id: int | None
+ group: str | None
+ group_id: int | None
+ working_dir: str | None
+ on_success: Literal["restart", "shutdown", "ignore"] | None
+ on_failure: Literal["restart", "shutdown", "ignore"] | None
+ on_check_failure: dict[str, Literal["restart", "shutdown", "ignore"]] | None
+ backoff_delay: str | None
+ backoff_factor: float | None
+ backoff_limit: str | None
+ kill_delay: str | None
class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""
allow_population_by_field_name = True
- alias_generator = lambda s: s.replace("_", "-") # noqa: E731
+ alias_generator: Callable[[str], str] = lambda s: s.replace( # noqa: E731
+ "_", "-"
+ )
extra = "forbid"
@@ -174,7 +181,7 @@ def define_pebble_layer(
self,
target_dir: Path,
ref_fs: Path,
- layer_content: Dict[str, Any],
+ layer_content: dict[str, Any],
rock_name: str,
) -> None:
"""Infers and defines a new Pebble layer file.
@@ -194,7 +201,7 @@ def define_pebble_layer(
pebble_layers_path_in_base + "/[0-9][0-9][0-9]-???*.yaml"
) + glob.glob(pebble_layers_path_in_base + "/[0-9][0-9][0-9]-???*.yml")
- prefixes = list(map(lambda l: Path(l).name[:3], existing_pebble_layers))
+ prefixes = [Path(l).name[:3] for l in existing_pebble_layers]
prefixes.sort()
emit.progress(
f"Found {len(existing_pebble_layers)} Pebble layers in the base's root filesystem"
diff --git a/rockcraft/plugins/python_plugin.py b/rockcraft/plugins/python_plugin.py
index 3f38c87d4..3e8cfac7a 100644
--- a/rockcraft/plugins/python_plugin.py
+++ b/rockcraft/plugins/python_plugin.py
@@ -18,10 +18,9 @@
import logging
from textwrap import dedent
-from typing import List, Optional
from craft_parts.plugins import python_plugin
-from overrides import override
+from overrides import override # type: ignore[reportUnknownVariableType]
logger = logging.getLogger(__name__)
@@ -76,10 +75,10 @@ class PythonPlugin(python_plugin.PythonPlugin):
@override
def _should_remove_symlinks(self) -> bool:
"""Overridden because for ubuntu bases we must always remove the symlinks."""
- return self._part_info.base != "bare"
+ return bool(self._part_info.base != "bare")
@override
- def _get_system_python_interpreter(self) -> Optional[str]:
+ def _get_system_python_interpreter(self) -> str | None:
"""Overridden because Python must always be provided by the parts."""
return None
@@ -89,9 +88,9 @@ def _get_script_interpreter(self) -> str:
return "#!/bin/${PARTS_PYTHON_INTERPRETER}"
@override
- def get_build_commands(self) -> List[str]:
+ def get_build_commands(self) -> list[str]:
"""Overridden to add a sitecustomize.py ."""
- commands = []
+ commands: list[str] = []
# Detect whether PARTS_PYTHON_INTERPRETER is a full path (not supported)
commands.append(
diff --git a/rockcraft/services/image.py b/rockcraft/services/image.py
index 055951f50..21ec4c5b0 100644
--- a/rockcraft/services/image.py
+++ b/rockcraft/services/image.py
@@ -16,8 +16,6 @@
"""Rockcraft Image Service."""
-from __future__ import annotations
-
from dataclasses import dataclass
from pathlib import Path
from typing import cast
@@ -48,7 +46,7 @@ def __init__(
project: models.Project,
work_dir: Path,
build_for: str,
- ):
+ ) -> None:
super().__init__(app, services, project=project)
self._work_dir = work_dir
diff --git a/rockcraft/services/lifecycle.py b/rockcraft/services/lifecycle.py
index 993c0dfba..10639f877 100644
--- a/rockcraft/services/lifecycle.py
+++ b/rockcraft/services/lifecycle.py
@@ -16,18 +16,17 @@
"""Rockcraft Lifecycle service."""
-from __future__ import annotations
-
import contextlib
from pathlib import Path
-from typing import cast
+from typing import Any, cast
from craft_application import LifecycleService
-from craft_archives import repo
+from craft_archives import repo # type: ignore[import-untyped]
from craft_cli import emit
-from craft_parts import Features, Step, callbacks
+from craft_parts import Features, LifecycleManager, Step, callbacks
from craft_parts.errors import CallbackRegistrationError
-from overrides import override
+from craft_parts.infos import ProjectInfo, StepInfo
+from overrides import override # type: ignore[reportUnknownVariableType]
from rockcraft import layers
from rockcraft.models.project import Project
@@ -85,7 +84,10 @@ def run(self, step_name: str | None, part_names: list[str] | None = None) -> Non
callbacks.unregister_all()
-def _install_package_repositories(package_repositories, lifecycle_manager) -> None:
+def _install_package_repositories(
+ package_repositories: list[dict[str, Any]] | None,
+ lifecycle_manager: LifecycleManager,
+) -> None:
"""Install package repositories in the environment."""
if not package_repositories:
emit.debug("No package repositories specified, none to install.")
@@ -99,7 +101,7 @@ def _install_package_repositories(package_repositories, lifecycle_manager) -> No
emit.progress("Package repositories installed")
-def _install_overlay_repositories(overlay_dir, project_info):
+def _install_overlay_repositories(overlay_dir: Path, project_info: ProjectInfo) -> None:
if project_info.base != "bare":
package_repositories = project_info.package_repositories
repo.install_in_root(
@@ -109,10 +111,12 @@ def _install_overlay_repositories(overlay_dir, project_info):
)
-def _post_prime_callback(step_info) -> bool:
+def _post_prime_callback(step_info: StepInfo) -> bool:
prime_dir = step_info.prime_dir
- files = step_info.state.files
base_layer_dir = step_info.rootfs_dir
+ files: set[str]
+
+ files = step_info.state.files if step_info.state else set()
layers.prune_prime_files(prime_dir, files, base_layer_dir)
return True
diff --git a/rockcraft/services/package.py b/rockcraft/services/package.py
index 501ac56be..84be70782 100644
--- a/rockcraft/services/package.py
+++ b/rockcraft/services/package.py
@@ -16,8 +16,6 @@
"""Rockcraft Package service."""
-from __future__ import annotations
-
import datetime
import pathlib
import typing
@@ -25,7 +23,7 @@
from craft_application import AppMetadata, PackageService, models, util
from craft_cli import emit
-from overrides import override
+from overrides import override # type: ignore[reportUnknownVariableType]
from rockcraft import errors, oci
from rockcraft.models import Project
@@ -41,7 +39,7 @@ class RockcraftPackageService(PackageService):
def __init__(
self,
app: AppMetadata,
- services: RockcraftServiceFactory,
+ services: "RockcraftServiceFactory",
*,
project: models.Project,
platform: str | None,
diff --git a/rockcraft/services/provider.py b/rockcraft/services/provider.py
index aa7ef5414..050dd221f 100644
--- a/rockcraft/services/provider.py
+++ b/rockcraft/services/provider.py
@@ -16,11 +16,8 @@
"""Rockcraft Provider service."""
-
-from __future__ import annotations
-
from craft_application import ProviderService
-from overrides import override
+from overrides import override # type: ignore[reportUnknownVariableType]
class RockcraftProviderService(ProviderService):
diff --git a/rockcraft/services/service_factory.py b/rockcraft/services/service_factory.py
index 0b3be7d47..20ae8bf98 100644
--- a/rockcraft/services/service_factory.py
+++ b/rockcraft/services/service_factory.py
@@ -16,8 +16,6 @@
"""Rockcraft Service Factory."""
-from __future__ import annotations
-
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -37,11 +35,11 @@ class RockcraftServiceFactory(ServiceFactory):
ImageClass: type[services.RockcraftImageService] = services.RockcraftImageService
# These are overrides of default ServiceFactory services
- LifecycleClass: type[
+ LifecycleClass: type[ # type: ignore[reportIncompatibleVariableOverride]
services.RockcraftLifecycleService
] = services.RockcraftLifecycleService
PackageClass: type[base_services.PackageService] = services.RockcraftPackageService
- ProviderClass: type[
+ ProviderClass: type[ # type: ignore[reportIncompatibleVariableOverride]
services.RockcraftProviderService
] = services.RockcraftProviderService
diff --git a/rockcraft/usernames.py b/rockcraft/usernames.py
index 105bdbf20..6593358a4 100644
--- a/rockcraft/usernames.py
+++ b/rockcraft/usernames.py
@@ -16,6 +16,7 @@
"""List of allowed shared usernames/UIDs (analogously to SnapD)."""
+
import pydantic
@@ -38,7 +39,7 @@ def _validate_run_user(cls, username: str) -> str:
return username
- def get_dict(self) -> dict:
+ def get_dict(self) -> dict[str, dict[str, int]]:
"""Cast the object into a dict using the username as the key."""
return {self.username: {"uid": self.uid}}
diff --git a/rockcraft/utils.py b/rockcraft/utils.py
index 913bc45cd..3368da8eb 100644
--- a/rockcraft/utils.py
+++ b/rockcraft/utils.py
@@ -22,15 +22,20 @@
import pathlib
import shutil
import sys
-from collections import namedtuple
from distutils.util import strtobool # pylint: disable=deprecated-module
-from typing import Optional
+from typing import NamedTuple
import rockcraft.errors
logger = logging.getLogger(__name__)
-OSPlatform = namedtuple("OSPlatform", "system release machine")
+
+class OSPlatform(NamedTuple):
+ """Tuple containing the OS platform information."""
+
+ system: str
+ release: str
+ machine: str
def is_managed_mode() -> bool:
@@ -54,7 +59,7 @@ def get_managed_environment_log_path() -> pathlib.Path:
return pathlib.Path("/tmp/rockcraft.log")
-def get_managed_environment_snap_channel() -> Optional[str]:
+def get_managed_environment_snap_channel() -> str | None:
"""User-specified channel to use when installing Rockcraft snap from Snap Store.
:returns: Channel string if specified, else None.
@@ -85,7 +90,7 @@ def confirm_with_user(prompt: str, default: bool = False) -> bool:
return reply[0] == "y" if reply else default
-def _find_command_path_in_root(root: str, command_name: str) -> Optional[str]:
+def _find_command_path_in_root(root: str, command_name: str) -> str | None:
"""Find the path of a command in a given root path."""
for bin_directory in (
"usr/local/sbin",
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 135ed889a..000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,132 +0,0 @@
-[metadata]
-name = rockcraft
-version = attr: rockcraft.__version__
-description="Create ROCKS"
-long_description = file: README.rst
-url = https://github.com/canonical/rockcraft
-project_urls =
- Documentation = https://rockcraft.readthedocs.io/en/latest/
- Source = https://github.com/canonical/rockcraft.git
- Issues = https://github.com/canonical/rockcraft/issues
-author = Canonical Ltd.
-author_email = snapcraft@lists.snapcraft.io
-license = GNU Lesser General Public License v3 (LGPLv3)
-license_files = LICENSE
-classifiers =
- Development Status :: 2 - Pre-Alpha
- Intended Audience :: Developers
- License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
- Operating System :: MacOS :: MacOS X
- Operating System :: POSIX :: Linux
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.10
-
-[options]
-python_requires = >= 3.10
-include_package_data = True
-packages = find:
-zip_safe = False
-install_requires =
- craft-application>=1.0.0
- craft-archives>=1.1.0
- craft-cli
- craft-parts
- craft-providers
- overrides
- spdx-lookup
- tabulate>=0.8.10
-
-[options.entry_points]
-console_scripts =
- rockcraft = rockcraft.cli:run
-
-[options.package_data]
-rockcraft = py.typed
-
-[options.extras_require]
-doc =
- furo
- sphinx<7
- sphinx-autobuild
- sphinx-autodoc-typehints
- sphinx-copybutton
- sphinx_design
- sphinx-lint
- sphinx-pydantic
- sphinx-rtd-theme
- pyspelling
-release =
- twine
- wheel
-test =
- mccabe<0.7.0 # to resolve version conflict
- coverage
- black
- codespell
- flake8>=4.0.1
- isort
- mypy
- pydocstyle
- pylint
- pylint-fixme-info
- pylint-pytest>=1.1.3
- pytest
- pytest-check>=2.0
- pytest-mock
- pytest-subprocess
- ruff==0.1.6
- tox
- types-requests
- types-setuptools
- types-pyyaml
- types-tabulate>=0.9.0.2
-dev =
- autoflake
- %(doc)s
- %(release)s
- %(test)s
-
-[options.packages.find]
-exclude =
- tests
- tests.*
-
-[bdist_wheel]
-universal = 1
-
-[codespell]
-quiet-level = 3
-skip = ./docs/_build,.direnv,.git,.mypy_cache,.pytest_cache,.venv,__pycache__,venv
-ignore-words-list = warmup,buildd,astroid
-
-[flake8]
-exclude = .direnv .git .mypy_cache .pytest_cache .venv __pycache__ venv
-max-line-length = 88
-# E203 whitespace before ':'
-# E501 line too long
-extend-ignore = E203,E501
-
-[autoflake]
-remove-all-unused-imports=true
-ignore-init-module-imports=true
-recursive=true
-in-place=true
-
-[pydantic-mypy]
-init_forbid_extra = True
-init_typed = True
-warn_required_dynamic_aliases = True
-warn_untyped_fields = True
-
-[pydocstyle]
-# D105 Missing docstring in magic method (reason: magic methods already have definitions)
-# D107 Missing docstring in __init__ (reason: documented in class docstring)
-# D203 1 blank line required before class docstring (reason: pep257 default)
-# D213 Multi-line docstring summary should start at the second line (reason: pep257 default)
-# D215 Section underline is over-indented (reason: pep257 default)
-ignore = D105, D107, D203, D213, D215
-
-[aliases]
-test = pytest
-
-[tool:pytest]
diff --git a/setup.py b/setup.py
old mode 100644
new mode 100755
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 9bf05c61f..3f456fd43 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -27,45 +27,42 @@ parts:
rockcraft-libs:
plugin: nil
build-attributes:
- - enable-patchelf
+ - enable-patchelf
stage-packages:
- - apt
- - apt-transport-https
- - apt-utils
- - binutils
- - gpg
- - gpgv
- - libpython3-stdlib
- - libpython3.10-stdlib
- - libpython3.10-minimal
- - python3-pip
- - python3-setuptools
- - python3-wheel
- - python3-venv
- - python3-minimal
- - python3-distutils
- - python3-pkg-resources
- - python3.10-minimal
- - fuse-overlayfs
+ - apt
+ - apt-transport-https
+ - apt-utils
+ - binutils
+ - gpg
+ - gpgv
+ - libpython3-stdlib
+ - libpython3.10-stdlib
+ - libpython3.10-minimal
+ - python3-pip
+ - python3-setuptools
+ - python3-wheel
+ - python3-venv
+ - python3-minimal
+ - python3-distutils
+ - python3-pkg-resources
+ - python3.10-minimal
+ - fuse-overlayfs
organize:
- "usr/bin/fuse-overlayfs": "libexec/rockcraft/fuse-overlayfs"
+ "usr/bin/fuse-overlayfs": "libexec/rockcraft/fuse-overlayfs"
rockcraft:
source: .
plugin: python
python-packages:
- - wheel
- - pip
- - setuptools
- python-requirements:
- - requirements-jammy.txt
- - requirements.txt
+ - wheel
+ - pip
+ - setuptools
build-attributes:
- - enable-patchelf
+ - enable-patchelf
build-environment:
- - "CFLAGS": "$(pkg-config python-3.10 yaml-0.1 --cflags)"
+ - "CFLAGS": "$(pkg-config python-3.10 yaml-0.1 --cflags)"
organize:
- bin/craftctl: libexec/rockcraft/craftctl
+ bin/craftctl: libexec/rockcraft/craftctl
override-build: |
${SNAP}/libexec/snapcraft/craftctl default
@@ -83,14 +80,14 @@ parts:
source: https://github.com/opencontainers/umoci.git
source-tag: v0.4.7
make-parameters:
- - umoci.static
+ - umoci.static
override-build: |
- make umoci.static
- mkdir "$CRAFT_PART_INSTALL"/bin
- install -m755 umoci.static "$CRAFT_PART_INSTALL"/bin/umoci
+ make umoci.static
+ mkdir "$CRAFT_PART_INSTALL"/bin
+ install -m755 umoci.static "$CRAFT_PART_INSTALL"/bin/umoci
build-packages:
- - golang-go
- - make
+ - golang-go
+ - make
skopeo:
plugin: nil
@@ -101,20 +98,20 @@ parts:
mkdir "$CRAFT_PART_INSTALL"/bin
install -m755 skopeo "$CRAFT_PART_INSTALL"/bin/skopeo
stage-packages:
- - libgpgme11
- - libassuan0
- - libbtrfs0
- - libdevmapper1.02.1
+ - libgpgme11
+ - libassuan0
+ - libbtrfs0
+ - libdevmapper1.02.1
build-attributes:
- - enable-patchelf
+ - enable-patchelf
build-snaps:
- - go/1.17/stable
+ - go/1.17/stable
build-packages:
- - libgpgme-dev
- - libassuan-dev
- - libbtrfs-dev
- - libdevmapper-dev
- - pkg-config
+ - libgpgme-dev
+ - libassuan-dev
+ - libbtrfs-dev
+ - libdevmapper-dev
+ - pkg-config
chisel:
plugin: go
diff --git a/spread.yaml b/spread.yaml
index 551aa1781..80d141f52 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -7,13 +7,12 @@ environment:
PATH: /snap/bin:$PATH:$SNAPD_TESTING_TOOLS:$PROJECT_PATH/tools/spread
include:
- - tests/
- - tools/
- - docs/
- - requirements-doc.txt
- - requirements-jammy.txt
- - Makefile
- - rockcraft/
+ - tests/
+ - tools/
+ - docs/
+ - pyproject.toml
+ - rockcraft/
+ - tox.ini
backends:
google:
@@ -81,11 +80,11 @@ prepare: |
snap install docker --channel=core18/stable
else
snap install docker
- fi
+ fi
# make sure docker is working
retry -n 10 --wait 2 sh -c 'docker run --rm hello-world'
-
+
install_rockcraft
restore-each: |
@@ -98,7 +97,7 @@ restore-each: |
lxc --project=rockcraft delete --force "$instance"
fi
done
- fi
+ fi
debug-each: |
# output latest rockcraft log file on test failure
@@ -118,7 +117,7 @@ suites:
docs/how-to/code/:
summary: tests how-to guides from the docs
systems:
- - ubuntu-22.04-64
+ - ubuntu-22.04-64
docs/reference/code/:
summary: tests reference code from the docs
@@ -130,3 +129,7 @@ suites:
summary: bigger tests that take longer to run
manual: true
+ tests/spread/integration/:
+ summary: tests for rockcraft integration that not work in tox
+ systems:
+ - ubuntu-22.04-64
diff --git a/tests/conftest.py b/tests/conftest.py
index 7f60d5fe9..12dd4ff2f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -21,7 +21,7 @@
import xdg # type: ignore
-@pytest.fixture
+@pytest.fixture()
def new_dir(tmpdir):
"""Change to a new temporary directory."""
@@ -103,16 +103,15 @@ def assert_recorded_raw(self, expected):
self._check(expected, self.raw)
-@pytest.fixture
+@pytest.fixture()
def extra_project_params():
"""Configuration fixture for the Project used by the default services."""
return {}
-@pytest.fixture
+@pytest.fixture()
def default_project(extra_project_params):
from craft_application.models import VersionStr
-
from rockcraft.models.project import NameStr, Project
parts = extra_project_params.pop("parts", {})
@@ -130,7 +129,7 @@ def default_project(extra_project_params):
)
-@pytest.fixture
+@pytest.fixture()
def default_factory(default_project):
from rockcraft.application import APP_METADATA
from rockcraft.services import RockcraftServiceFactory
@@ -143,7 +142,7 @@ def default_factory(default_project):
return factory
-@pytest.fixture
+@pytest.fixture()
def default_image_info():
from rockcraft import oci
from rockcraft.services.image import ImageInfo
@@ -155,14 +154,14 @@ def default_image_info():
)
-@pytest.fixture
+@pytest.fixture()
def default_application(default_factory, default_project):
from rockcraft.application import APP_METADATA, Rockcraft
return Rockcraft(APP_METADATA, default_factory)
-@pytest.fixture
+@pytest.fixture()
def image_service(default_project, default_factory, tmp_path):
from rockcraft.application import APP_METADATA
from rockcraft.services import RockcraftImageService
@@ -176,7 +175,7 @@ def image_service(default_project, default_factory, tmp_path):
)
-@pytest.fixture
+@pytest.fixture()
def provider_service(default_project, default_factory, tmp_path):
from rockcraft.application import APP_METADATA
from rockcraft.services import RockcraftProviderService
@@ -189,7 +188,7 @@ def provider_service(default_project, default_factory, tmp_path):
)
-@pytest.fixture
+@pytest.fixture()
def package_service(default_project, default_factory):
from rockcraft.application import APP_METADATA
from rockcraft.services import RockcraftPackageService
@@ -203,7 +202,7 @@ def package_service(default_project, default_factory):
)
-@pytest.fixture
+@pytest.fixture()
def lifecycle_service(default_project, default_factory):
from rockcraft.application import APP_METADATA
from rockcraft.services import RockcraftLifecycleService
@@ -218,14 +217,14 @@ def lifecycle_service(default_project, default_factory):
)
-@pytest.fixture
+@pytest.fixture()
def mock_obtain_image(default_factory, mocker):
"""Mock and return the "obtain_image()" method of the default image service."""
image_service = default_factory.image
return mocker.patch.object(image_service, "obtain_image")
-@pytest.fixture
+@pytest.fixture()
def run_lifecycle(mocker):
"""Helper to call testing.run_mocked_lifecycle()."""
diff --git a/tests/integration/plugins/test_python_plugin.py b/tests/integration/plugins/test_python_plugin.py
index 862350d74..e983eca46 100644
--- a/tests/integration/plugins/test_python_plugin.py
+++ b/tests/integration/plugins/test_python_plugin.py
@@ -23,10 +23,10 @@
from craft_cli import EmitterMode, emit
from craft_parts.errors import OsReleaseVersionIdError
from craft_parts.utils.os_utils import OsRelease
-
from rockcraft import plugins
from rockcraft.models.project import Project
from rockcraft.plugins.python_plugin import SITECUSTOMIZE_TEMPLATE
+
from tests.testing.project import create_project
from tests.util import ubuntu_only
@@ -70,7 +70,7 @@ def create_python_project(base, extra_part_props=None) -> Project:
class ExpectedValues:
"""Expected venv Python values for a given Ubuntu host."""
- symlinks: typing.List[str]
+ symlinks: list[str]
symlink_target: str
version_dir: str
@@ -99,6 +99,7 @@ class ExpectedValues:
VALUES_FOR_HOST = RELEASE_TO_VALUES["22.04"]
+@pytest.mark.notox()
@pytest.mark.parametrize("base", tuple(UBUNTU_BASES))
def test_python_plugin_ubuntu(base, tmp_path, run_lifecycle):
project = create_python_project(base=base)
@@ -111,9 +112,13 @@ def test_python_plugin_ubuntu(base, tmp_path, run_lifecycle):
assert list(bin_dir.glob("python*")) == []
# Check the shebang in the "hello" script
+ # In test env this could be replaced with sh that exec python by the venv
expected_shebang = "#!/bin/python3"
hello = bin_dir / "hello"
- assert hello.read_text().startswith(expected_shebang)
+ hello_text = hello.read_text()
+ assert hello_text.startswith(expected_shebang)
+ if hello_text.startswith("#!/bin/sh"):
+ assert "bin/python" in hello_text
# Check the extra sitecustomize.py module that we add
expected_text = SITECUSTOMIZE_TEMPLATE.replace("EOF", "")
@@ -128,6 +133,7 @@ def test_python_plugin_ubuntu(base, tmp_path, run_lifecycle):
assert not pyvenv_cfg.is_file()
+@pytest.mark.notox()
def test_python_plugin_bare(tmp_path, run_lifecycle):
project = create_python_project(base="bare")
run_lifecycle(project=project, work_dir=tmp_path)
@@ -146,9 +152,13 @@ def test_python_plugin_bare(tmp_path, run_lifecycle):
)
# Check the shebang in the "hello" script
+ # In test env this could be replaced with sh that exec python by the venv
expected_shebang = "#!/bin/python3"
hello = bin_dir / "hello"
- assert hello.read_text().startswith(expected_shebang)
+ hello_text = hello.read_text()
+ assert hello_text.startswith(expected_shebang)
+ if hello_text.startswith("#!/bin/sh"):
+ assert "bin/python" in hello_text
# Check the extra sitecustomize.py module that we add
expected_text = SITECUSTOMIZE_TEMPLATE.replace("EOF", "")
diff --git a/tests/integration/services/test_lifecycle.py b/tests/integration/services/test_lifecycle.py
index b8a042856..1bc674ff4 100644
--- a/tests/integration/services/test_lifecycle.py
+++ b/tests/integration/services/test_lifecycle.py
@@ -19,8 +19,8 @@
import pytest
from craft_parts import overlays
-
from rockcraft.services import lifecycle
+
from tests.testing.project import create_project
from tests.util import jammy_only
diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py
index 0532514cb..61ee59fc7 100644
--- a/tests/integration/test_application.py
+++ b/tests/integration/test_application.py
@@ -18,10 +18,10 @@
import pytest
import yaml
-
from rockcraft.application import APP_METADATA, Rockcraft
from rockcraft.services import RockcraftServiceFactory
from rockcraft.services.image import ImageInfo, RockcraftImageService
+
from tests.util import jammy_only
pytestmark = [jammy_only, pytest.mark.usefixtures("reset_callbacks")]
diff --git a/tests/integration/test_oci.py b/tests/integration/test_oci.py
index 0f49a1fd1..b9bc9513e 100644
--- a/tests/integration/test_oci.py
+++ b/tests/integration/test_oci.py
@@ -17,13 +17,13 @@
import subprocess
import tarfile
import textwrap
+from collections.abc import Callable
from pathlib import Path
-from typing import Callable, List, Tuple
import pytest
-
from rockcraft import oci
from rockcraft.services.image import ImageInfo
+
from tests.util import jammy_only
pytestmark = jammy_only
@@ -31,7 +31,7 @@
def create_base_image(
work_dir: Path, populate_base_layer: Callable[[Path], None]
-) -> Tuple[oci.Image, Path]:
+) -> tuple[oci.Image, Path]:
"""Create a base image with content provided by a callable.
This function will create an empty image, extract it to a bundle and then
@@ -72,7 +72,7 @@ def create_base_image(
return image, base_layer_dir
-def get_names_in_layer(image: oci.Image, layer_number: int = -1) -> List[str]:
+def get_names_in_layer(image: oci.Image, layer_number: int = -1) -> list[str]:
"""Get the list of file/dir names contained in the given layer, sorted."""
umoci_stat = image.stat()
@@ -111,7 +111,7 @@ def populate_base_layer(base_layer_dir):
new_layer_dir = Path("new")
new_layer_dir.mkdir()
- for target in targets + ["tmp"]:
+ for target in [*targets, "tmp"]:
new_target_dir = new_layer_dir / target
new_target_dir.mkdir()
(new_target_dir / f"new_{target}_file").write_text(f"new {target} file")
@@ -128,7 +128,7 @@ def populate_base_layer(base_layer_dir):
]
-@pytest.fixture
+@pytest.fixture()
def extra_project_params():
"""Fixture used to configure the Project used by the default test services."""
return {
diff --git a/tests/spread/general/bare-base/task.yaml b/tests/spread/general/bare-base/task.yaml
index 84b912866..69bd4ac67 100644
--- a/tests/spread/general/bare-base/task.yaml
+++ b/tests/spread/general/bare-base/task.yaml
@@ -15,5 +15,5 @@ execute: |
grep_docker_log "$id" "ship it!"
docker exec "$id" pebble services | grep hello
docker exec "$id" pebble ls /usr/bin/hello
-
+
docker rm -f "$id"
diff --git a/tests/spread/general/big/rockcraft.yaml b/tests/spread/general/big/rockcraft.yaml
index d0cd87a28..6f3cacbf5 100644
--- a/tests/spread/general/big/rockcraft.yaml
+++ b/tests/spread/general/big/rockcraft.yaml
@@ -3,10 +3,10 @@ version: latest
summary: A big ROCK to test many features
description: |
A big ROCK whose purpose is to test many features while only paying the "setup"
- and "teardown" price once. Feel free to add to this file and to "task.yaml",
+ and "teardown" price once. Feel free to add to this file and to "task.yaml",
adding references to issues/PRs where appropriate.
license: Apache-2.0
-base: ubuntu:22.04 # Leaving ":"-notation on purpose here
+base: ubuntu:22.04 # Leaving ":"-notation on purpose here
platforms:
amd64:
@@ -18,7 +18,7 @@ services:
on-success: shutdown
on-failure: shutdown
working-dir: /tmp
-
+
parts:
issue-44-dir-owner:
plugin: dump
@@ -52,4 +52,3 @@ parts:
mkdir fake_rock_dir
touch fake_rock_dir/fake_rock_file
ln -s fake_rock_dir .rock
-
diff --git a/tests/spread/general/big/task.yaml b/tests/spread/general/big/task.yaml
index a52273150..ff06de04a 100644
--- a/tests/spread/general/big/task.yaml
+++ b/tests/spread/general/big/task.yaml
@@ -28,7 +28,7 @@ execute: |
# Check the ROCK's output
docker run --rm big:latest | MATCH "/tmp"
-
+
############################################################################################
# test ownership: "newfiles" and "a.txt" are owned by uid 9999, "b.txt" is owned by uid 3333
# (github issue #44)
@@ -54,7 +54,7 @@ execute: |
file pebble | grep "statically linked"
docker rm -f big-container
rm pebble
-
+
############################################################################################
# This check documents the fact that we currently don't preserve/observe symlinks between
# layers - we only take the base on which the ROCK was built into account. If the behavior
diff --git a/tests/spread/general/chisel/rockcraft.yaml b/tests/spread/general/chisel/rockcraft.yaml
index 0dd12ace8..d9c7a6787 100644
--- a/tests/spread/general/chisel/rockcraft.yaml
+++ b/tests/spread/general/chisel/rockcraft.yaml
@@ -7,7 +7,7 @@ version: "0.0.1"
base: bare
build_base: ubuntu@22.04
run-user: _daemon_
-services:
+services:
dotnet:
override: replace
command: /usr/lib/dotnet/dotnet [ --info ]
diff --git a/tests/spread/general/clean/rockcraft.yaml b/tests/spread/general/clean/rockcraft.yaml
index 061142057..aa6f5c111 100644
--- a/tests/spread/general/clean/rockcraft.yaml
+++ b/tests/spread/general/clean/rockcraft.yaml
@@ -10,4 +10,3 @@ platforms:
parts:
my-part:
plugin: nil
-
diff --git a/tests/spread/general/craftctl/rockcraft.yaml b/tests/spread/general/craftctl/rockcraft.yaml
index 4a76d2349..db731c916 100644
--- a/tests/spread/general/craftctl/rockcraft.yaml
+++ b/tests/spread/general/craftctl/rockcraft.yaml
@@ -8,7 +8,7 @@ platforms:
craftctl:
build-on: ["amd64", "i386"]
build-for: amd64
-
+
parts:
hello:
plugin: make
diff --git a/tests/spread/general/craftctl/task.yaml b/tests/spread/general/craftctl/task.yaml
index 82fe1331f..2141a6a52 100644
--- a/tests/spread/general/craftctl/task.yaml
+++ b/tests/spread/general/craftctl/task.yaml
@@ -17,4 +17,3 @@ execute: |
docker run --rm --entrypoint /usr/bin/hello craftctl-test:latest
docker run --rm craftctl-test:latest help
docker run --rm craftctl-test:latest --help
-
\ No newline at end of file
diff --git a/tests/spread/general/destructive/rockcraft.yaml b/tests/spread/general/destructive/rockcraft.yaml
index a76f992d8..a76820a77 100644
--- a/tests/spread/general/destructive/rockcraft.yaml
+++ b/tests/spread/general/destructive/rockcraft.yaml
@@ -3,7 +3,7 @@ version: latest
summary: A destructively-built ROCK
description: Building a ROCK in destructive mode
license: Apache-2.0
-build-base: ubuntu:22.04 # Leaving ":"-notation on purpose here
+build-base: ubuntu:22.04 # Leaving ":"-notation on purpose here
base: bare
platforms:
amd64:
diff --git a/tests/spread/general/destructive/task.yaml b/tests/spread/general/destructive/task.yaml
index 7e51f472c..52b4aaff7 100644
--- a/tests/spread/general/destructive/task.yaml
+++ b/tests/spread/general/destructive/task.yaml
@@ -3,16 +3,16 @@ summary: destructive-mode test
execute: |
# Check that work dirs *don't* exist
test ! -d parts -a ! -d stage -a ! -d prime
-
+
run_rockcraft pack --destructive-mode
test -f destructive-mode*.rock
-
+
# Check that work dirs *do* exist
test -d parts -a -d stage -a -d prime
-
+
run_rockcraft clean --destructive-mode
-
+
# Check that work dirs *don't* exist again
test ! -d parts -a ! -d stage -a ! -d prime
diff --git a/tests/spread/general/entrypoint-service/task.yaml b/tests/spread/general/entrypoint-service/task.yaml
index 8af1dba18..31882dd54 100644
--- a/tests/spread/general/entrypoint-service/task.yaml
+++ b/tests/spread/general/entrypoint-service/task.yaml
@@ -7,7 +7,7 @@ execute: |
# test container execution
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:entrypoint-service-test_latest_amd64.rock docker-daemon:entrypoint-service-test:latest
- rm entrypoint-service-test_latest*.rock
+ rm entrypoint-service-test_latest*.rock
docker images entrypoint-service-test:latest
id=$(docker run -d entrypoint-service-test)
test "$(docker inspect "$id" -f '{{json .Config.Entrypoint}}')" = '["/bin/pebble","enter","--verbose","--args","test-service"]'
diff --git a/tests/spread/general/environment/rockcraft.yaml b/tests/spread/general/environment/rockcraft.yaml
index 64da79e9d..cd0b10294 100644
--- a/tests/spread/general/environment/rockcraft.yaml
+++ b/tests/spread/general/environment/rockcraft.yaml
@@ -21,7 +21,7 @@ platforms:
amd64v2:
build-on: ["amd64", "i386"]
build-for: amd64
-
+
parts:
part1:
plugin: nil
diff --git a/tests/spread/general/environment/task.yaml b/tests/spread/general/environment/task.yaml
index 31a3b23fd..83d41e899 100644
--- a/tests/spread/general/environment/task.yaml
+++ b/tests/spread/general/environment/task.yaml
@@ -10,7 +10,7 @@ execute: |
# test container execution
docker images
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:environment-test_latest_amd64v2.rock docker-daemon:environment-test:latest
- rm environment-test_latest*.rock
+ rm environment-test_latest*.rock
docker images environment-test:latest
id=$(docker run --rm -d environment-test -v)
grep_docker_log "$id" "X=ship it!"
diff --git a/tests/spread/general/health-checks/rockcraft.yaml b/tests/spread/general/health-checks/rockcraft.yaml
index 562815209..3cf38a02a 100644
--- a/tests/spread/general/health-checks/rockcraft.yaml
+++ b/tests/spread/general/health-checks/rockcraft.yaml
@@ -6,7 +6,7 @@ license: Apache-2.0
version: latest
base: ubuntu@22.04
-services:
+services:
webserver:
override: replace
command: timeout 30 nginx -g 'daemon off;'
@@ -39,7 +39,7 @@ package-repositories:
- type: apt
url: https://nginx.org/packages/mainline/ubuntu
key-id: 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
- suites:
+ suites:
- jammy
components:
- nginx
diff --git a/tests/spread/general/invalid-name/task.yaml b/tests/spread/general/invalid-name/task.yaml
index c44d99b56..b5ced0587 100644
--- a/tests/spread/general/invalid-name/task.yaml
+++ b/tests/spread/general/invalid-name/task.yaml
@@ -5,4 +5,4 @@ execute: |
do
sed "s/placeholder-name/$name/" rockcraft.orig.yaml > rockcraft.yaml
rockcraft pack 2>&1 >/dev/null | MATCH "Invalid name for ROCK"
- done
\ No newline at end of file
+ done
diff --git a/tests/spread/general/plugin-go/task.yaml b/tests/spread/general/plugin-go/task.yaml
index c3e88fb74..e29eab3a2 100644
--- a/tests/spread/general/plugin-go/task.yaml
+++ b/tests/spread/general/plugin-go/task.yaml
@@ -2,4 +2,3 @@ summary: check that the build-snap used by the go plugin does not interfere with
execute: |
run_rockcraft prime
-
diff --git a/tests/spread/general/plugin-python-3.6/rockcraft.orig.yaml b/tests/spread/general/plugin-python-3.6/rockcraft.orig.yaml
index df4c47edb..6b99d9375 100644
--- a/tests/spread/general/plugin-python-3.6/rockcraft.orig.yaml
+++ b/tests/spread/general/plugin-python-3.6/rockcraft.orig.yaml
@@ -9,9 +9,9 @@ platforms:
amd64:
package-repositories:
- - type: apt
- ppa: deadsnakes/ppa
- priority: always
+ - type: apt
+ ppa: deadsnakes/ppa
+ priority: always
parts:
my-part:
diff --git a/tests/spread/general/plugin-python/base-2204/rockcraft.yaml b/tests/spread/general/plugin-python/base-2204/rockcraft.yaml
index f02ddd2c7..6a74d6a53 100644
--- a/tests/spread/general/plugin-python/base-2204/rockcraft.yaml
+++ b/tests/spread/general/plugin-python/base-2204/rockcraft.yaml
@@ -2,4 +2,4 @@ name: base-2204
base: ubuntu@22.04
# Remaining contents will come from "parts.yaml"
-#'/usr/lib/python3/dist-packages', '/lib/python3.10/site-packages'
\ No newline at end of file
+# '/usr/lib/python3/dist-packages', '/lib/python3.10/site-packages'
diff --git a/tests/spread/general/plugin-python/parts.yaml b/tests/spread/general/plugin-python/parts.yaml
index 77684e9cf..5ebd0ee0c 100644
--- a/tests/spread/general/plugin-python/parts.yaml
+++ b/tests/spread/general/plugin-python/parts.yaml
@@ -1,4 +1,3 @@
-
# Actual parts definition for the rockcraft + python cases that we are testing.
# (this gets appended to all rockcraft.yaml files in the subdirectories in the
# main task.yaml).
diff --git a/tests/spread/general/plugin-python/task.yaml b/tests/spread/general/plugin-python/task.yaml
index 8866dfd30..d1511c97f 100644
--- a/tests/spread/general/plugin-python/task.yaml
+++ b/tests/spread/general/plugin-python/task.yaml
@@ -8,40 +8,40 @@ execute: |
SCENARIO_DIR="${SCENARIO}"
ROCK_FILE="${SCENARIO}_0.1_amd64.rock"
IMAGE="${SCENARIO}:0.1"
-
+
# change into the scenario's directory
cd ${SCENARIO_DIR}
-
+
# add the parts definition, common to all scenarios
cat ../parts.yaml >> rockcraft.yaml
-
+
# copy the Python source of the project we're building (also shared)
cp -r ../src .
-
+
# Build the ROCK & load it into docker
run_rockcraft pack
test -f ${ROCK_FILE}
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:${ROCK_FILE} docker-daemon:${IMAGE}
docker images
rm ${ROCK_FILE}
-
+
# Run the packaged project, both via the console script and via "python -m"
docker run --rm $IMAGE exec hello | MATCH "hello world"
docker run --rm $IMAGE exec /bin/python3 -m hello | MATCH "hello world"
docker run --rm $IMAGE exec /usr/bin/python3 -m hello | MATCH "hello world"
docker run --rm $IMAGE exec python3 -m hello | MATCH "hello world"
-
+
# Run the extra Python package, installed as a python-package, to make sure it's found
docker run --rm $IMAGE exec black --version
docker run --rm $IMAGE exec /bin/python3 -m black --version
docker run --rm $IMAGE exec /usr/bin/python3 -m black --version
docker run --rm $IMAGE exec python3 -m black --version
-
+
# Run the extra Python dist-package, installed as a stage-package, to make sure it's found
docker run --rm $IMAGE exec /bin/python3 -m cpuinfo --help
docker run --rm $IMAGE exec /usr/bin/python3 -m cpuinfo --help
docker run --rm $IMAGE exec python3 -m cpuinfo --help
-
+
# Run "check-pythonpath.py" to make sure the ordering of the packages dirs is correct
docker run --rm $IMAGE exec /bin/python3 /check-pythonpath.py
docker run --rm $IMAGE exec /usr/bin/python3 /check-pythonpath.py
diff --git a/tests/spread/general/prune/task.yaml b/tests/spread/general/prune/task.yaml
index 4a66f0b2d..e5f0fc163 100644
--- a/tests/spread/general/prune/task.yaml
+++ b/tests/spread/general/prune/task.yaml
@@ -4,7 +4,7 @@ execute: |
run_rockcraft pack
test -f ./*.rock
-
+
# Unpack the ROCK and verify that the lifecycle-based layer has no files in
# common with the base Ubuntu layer.
tar -xf ./*.rock
diff --git a/tests/spread/general/repo-bare-base/task.yaml b/tests/spread/general/repo-bare-base/task.yaml
index 794a5bf1d..0162d67e7 100644
--- a/tests/spread/general/repo-bare-base/task.yaml
+++ b/tests/spread/general/repo-bare-base/task.yaml
@@ -12,7 +12,7 @@ execute: |
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:apt-repo-static-test_latest_amd64.rock docker-daemon:apt-repo-static-test:latest
# Ensure container exists
docker images apt-repo-static-test | MATCH "apt-repo-static-test"
-
+
docker run --rm apt-repo-static-test exec /bin/bash-static /usr/bin/test-ppa | MATCH "hello!"
restore: |
diff --git a/tests/spread/general/repo-ubuntu-base/task.yaml b/tests/spread/general/repo-ubuntu-base/task.yaml
index 2dc789e63..be4c10f32 100644
--- a/tests/spread/general/repo-ubuntu-base/task.yaml
+++ b/tests/spread/general/repo-ubuntu-base/task.yaml
@@ -12,9 +12,9 @@ execute: |
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:apt-repo-test_latest_amd64.rock docker-daemon:apt-repo-test:latest
# Ensure container exists
docker images apt-repo-test | MATCH "apt-repo-test"
-
+
docker run --rm apt-repo-test exec /usr/bin/python3.12 -c "import sys;print(sys.version)" | MATCH "3.12"
-
+
restore: |
rm -f apt-repo-test_latest_amd64.rock
docker rmi -f apt-repo-test
diff --git a/tests/spread/general/run-user/rockcraft.yaml b/tests/spread/general/run-user/rockcraft.yaml
index 1c03db75b..5406ec807 100644
--- a/tests/spread/general/run-user/rockcraft.yaml
+++ b/tests/spread/general/run-user/rockcraft.yaml
@@ -21,4 +21,3 @@ parts:
overlay-script: |
set -x
useradd -R $CRAFT_OVERLAY -M -U -r test-user
-
\ No newline at end of file
diff --git a/tests/spread/general/run-user/task.yaml b/tests/spread/general/run-user/task.yaml
index 5b5571653..b5e8de0c8 100644
--- a/tests/spread/general/run-user/task.yaml
+++ b/tests/spread/general/run-user/task.yaml
@@ -13,7 +13,7 @@ execute: |
# Ensure container exists
docker images run-user-test | MATCH "run-user-test"
docker inspect run-user-test --format '{{.Config.User}}' | MATCH "_daemon_"
-
+
# ensure container doesn't exist
docker rm -f run-user-test-container
docker run --rm --entrypoint /bin/sh run-user-test -c 'whoami' | MATCH "_daemon_"
diff --git a/tests/spread/general/usrmerge/rockcraft.yaml b/tests/spread/general/usrmerge/rockcraft.yaml
index 0cb855d02..c94ee105e 100644
--- a/tests/spread/general/usrmerge/rockcraft.yaml
+++ b/tests/spread/general/usrmerge/rockcraft.yaml
@@ -14,11 +14,11 @@ parts:
# This build script adds a file in bin/ - the symlink in the base should be preserved.
mkdir ${CRAFT_PART_INSTALL}/bin
touch ${CRAFT_PART_INSTALL}/bin/new_bin_file
-
+
# Also add subdirectories in bin/ to make sure they are correctly handled.
mkdir -p ${CRAFT_PART_INSTALL}/bin/subdir1/subdir2
touch ${CRAFT_PART_INSTALL}/bin/subdir1/subdir2/subdir_bin_file
-
+
# Also add the same subdirectory structure in usr/bin/ to make sure they are not
# duplicated in the layer file
mkdir -p ${CRAFT_PART_INSTALL}/usr/bin/subdir1/subdir2
diff --git a/tests/spread/general/usrmerge/task.yaml b/tests/spread/general/usrmerge/task.yaml
index ab4dde6ec..8829a45be 100644
--- a/tests/spread/general/usrmerge/task.yaml
+++ b/tests/spread/general/usrmerge/task.yaml
@@ -13,13 +13,13 @@ execute: |
rm "$ROCK"
docker images usrmerge:latest
-
+
############################################################################################
# check that the /bin symlink to /usr/bin *was not* broken by the /bin/new_bin_file addition
############################################################################################
docker run --rm usrmerge exec readlink /bin | MATCH usr/bin
docker run --rm usrmerge exec ls /bin/bash /bin/new_bin_file /bin/subdir1/subdir2/subdir_bin_file
-
+
############################################################################################
# check that the contents of bin/ and usr/bin *both* ended up in /usr/bin
############################################################################################
diff --git a/tests/spread/integration/shebang/task.yaml b/tests/spread/integration/shebang/task.yaml
new file mode 100644
index 000000000..636c38de2
--- /dev/null
+++ b/tests/spread/integration/shebang/task.yaml
@@ -0,0 +1,13 @@
+summary: test shebang handling (not working in tox)
+
+execute: |
+ tests.pkgs install tox libapt-pkg-dev
+
+ pushd $PROJECT_PATH/
+
+ git init
+ git add .
+ git commit -m "test"
+
+ tox run --colored yes -m tests --notest
+ ./.tox/integration-py3.10/bin/pytest -m notox tests/integration/
diff --git a/tests/testing/lifecycle.py b/tests/testing/lifecycle.py
index c9b542766..018525b39 100644
--- a/tests/testing/lifecycle.py
+++ b/tests/testing/lifecycle.py
@@ -14,7 +14,6 @@
# You should have received a copy of the GNU General Public License along
# with this program. If not, see .
"""Project-related utility functions for running lifecycles."""
-from __future__ import annotations
import pathlib
from typing import cast
diff --git a/tests/unit/commands/test_expand_extensions.py b/tests/unit/commands/test_expand_extensions.py
index 77124547f..219061418 100644
--- a/tests/unit/commands/test_expand_extensions.py
+++ b/tests/unit/commands/test_expand_extensions.py
@@ -18,9 +18,9 @@
from pathlib import Path
import pytest
-
from rockcraft import extensions
from rockcraft.commands import ExpandExtensionsCommand
+
from tests.unit.testing.extensions import FULL_EXTENSION_YAML, FullExtension
# The project with the extension (FullExtension) expanded
@@ -67,7 +67,7 @@
)
-@pytest.fixture
+@pytest.fixture()
def setup_extensions(mock_extensions):
extensions.register(FullExtension.NAME, FullExtension)
diff --git a/tests/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py
index e9db67c1f..da3a64467 100644
--- a/tests/unit/commands/test_list_extensions.py
+++ b/tests/unit/commands/test_list_extensions.py
@@ -17,13 +17,13 @@
from textwrap import dedent
import pytest
-
from rockcraft import extensions
from rockcraft.commands import ExtensionsCommand, ListExtensionsCommand
+
from tests.unit.testing.extensions import ExperimentalExtension, FakeExtension
-@pytest.fixture
+@pytest.fixture()
def setup_extensions(mock_extensions):
extensions.register(FakeExtension.NAME, FakeExtension)
extensions.register(ExperimentalExtension.NAME, ExperimentalExtension)
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index c02d00700..dce48d404 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -16,7 +16,6 @@
import contextlib
from pathlib import Path
-from typing import Optional
from unittest import mock
import pytest
@@ -25,14 +24,13 @@
# pylint: disable=import-outside-toplevel
-@pytest.fixture
+@pytest.fixture()
def mock_instance():
"""Provide a mock instance (Executor)."""
- _mock_instance = mock.Mock(spec=Executor)
- yield _mock_instance
+ return mock.Mock(spec=Executor)
-@pytest.fixture
+@pytest.fixture()
def mock_extensions(monkeypatch):
from rockcraft.extensions import registry
@@ -78,7 +76,7 @@ def launched_environment(
project_name: str,
project_path: Path,
base_configuration: base.Base,
- build_base: Optional[str] = None,
+ build_base: str | None = None,
instance_name: str,
allow_unstable: bool = False,
):
diff --git a/tests/unit/extensions/test_extensions.py b/tests/unit/extensions/test_extensions.py
index f1985e278..df2112673 100644
--- a/tests/unit/extensions/test_extensions.py
+++ b/tests/unit/extensions/test_extensions.py
@@ -15,9 +15,9 @@
# along with this program. If not, see .
import pytest
-
from rockcraft import errors, extensions
from rockcraft.models import load_project
+
from tests.unit.testing.extensions import (
FULL_EXTENSION_YAML,
ExperimentalExtension,
@@ -27,7 +27,7 @@
)
-@pytest.fixture
+@pytest.fixture()
def fake_extensions(mock_extensions):
extensions.register(FakeExtension.NAME, FakeExtension)
extensions.register(ExperimentalExtension.NAME, ExperimentalExtension)
@@ -35,7 +35,7 @@ def fake_extensions(mock_extensions):
extensions.register(FullExtension.NAME, FullExtension)
-@pytest.fixture
+@pytest.fixture()
def input_yaml():
return {"base": "ubuntu@22.04"}
diff --git a/tests/unit/extensions/test_registry.py b/tests/unit/extensions/test_registry.py
index 5839e9faa..352c92cbe 100644
--- a/tests/unit/extensions/test_registry.py
+++ b/tests/unit/extensions/test_registry.py
@@ -14,7 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import pytest
-
from rockcraft import errors, extensions
from rockcraft.extensions.extension import Extension
@@ -37,7 +36,7 @@ class FakeExtension3(Extension):
NAME = "fake-extension-3"
-@pytest.fixture
+@pytest.fixture()
def fake_extensions(mock_extensions):
for ext_class in (FakeExtension1, FakeExtension2):
extensions.register(ext_class.NAME, ext_class)
diff --git a/tests/unit/plugins/test_python_plugin.py b/tests/unit/plugins/test_python_plugin.py
index 3c9e477d8..4ab9c1e31 100644
--- a/tests/unit/plugins/test_python_plugin.py
+++ b/tests/unit/plugins/test_python_plugin.py
@@ -20,9 +20,9 @@
import pytest
from craft_parts import Part, PartInfo, ProjectInfo
-
from rockcraft.models.project import Project
from rockcraft.plugins import PythonPlugin
+
from tests.util import ubuntu_only
pytestmark = ubuntu_only
diff --git a/tests/unit/services/test_lifecycle.py b/tests/unit/services/test_lifecycle.py
index b5451be58..c734cb2c8 100644
--- a/tests/unit/services/test_lifecycle.py
+++ b/tests/unit/services/test_lifecycle.py
@@ -18,13 +18,12 @@
import pytest
from craft_parts import LifecycleManager, callbacks
-
from rockcraft.services import lifecycle as lifecycle_module
# pylint: disable=protected-access
-@pytest.fixture
+@pytest.fixture()
def extra_project_params():
return {"package_repositories": [{"type": "apt", "ppa": "ppa/ppa"}]}
@@ -47,7 +46,7 @@ def test_lifecycle_args(lifecycle_service, default_factory, default_image_info,
application_name="rockcraft",
arch="x86_64",
base="ubuntu@22.04",
- base_layer_dir=Path("."),
+ base_layer_dir=Path(),
base_layer_hash=b"deadbeef",
cache_dir=Path("cache"),
ignore_local_sources=["*.rock"],
@@ -55,7 +54,7 @@ def test_lifecycle_args(lifecycle_service, default_factory, default_image_info,
project_name="default",
project_vars={"version": "1.0"},
work_dir=Path("work"),
- rootfs_dir=Path("."),
+ rootfs_dir=Path(),
)
diff --git a/tests/unit/services/test_package.py b/tests/unit/services/test_package.py
index 9d3bfdc92..2434a5339 100644
--- a/tests/unit/services/test_package.py
+++ b/tests/unit/services/test_package.py
@@ -35,7 +35,7 @@ def test_pack(package_service, default_factory, default_image_info, mocker):
# parameters.
mock_inner_pack.assert_called_once_with(
base_digest=b"deadbeef",
- base_layer_dir=Path("."),
+ base_layer_dir=Path(),
build_for="amd64",
prime_dir=Path("prime"),
project=default_factory.project,
diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py
index 740d3280d..6ca7fcf40 100644
--- a/tests/unit/test_cli.py
+++ b/tests/unit/test_cli.py
@@ -21,13 +21,12 @@
import pytest
import yaml
from craft_cli import emit
-
from rockcraft import cli, services
from rockcraft.application import Rockcraft
from rockcraft.models import project
-@pytest.fixture
+@pytest.fixture()
def lifecycle_init_mock():
"""Mock for ui.init."""
patcher = patch("rockcraft.commands.init.init")
@@ -66,7 +65,7 @@ def test_run_pack_services(mocker, monkeypatch, tmp_path):
lifecycle_mocks["run"].assert_called_once_with(step_name="prime", part_names=[])
package_mocks["write_metadata"].assert_called_once_with(fake_prime_dir)
- package_mocks["pack"].assert_called_once_with(fake_prime_dir, Path("."))
+ package_mocks["pack"].assert_called_once_with(fake_prime_dir, Path())
assert mock_ended_ok.called
assert log_path.is_file()
diff --git a/tests/unit/test_layers.py b/tests/unit/test_layers.py
index a6aae9e39..c2eb93f74 100644
--- a/tests/unit/test_layers.py
+++ b/tests/unit/test_layers.py
@@ -19,20 +19,18 @@
import sys
import tarfile
from pathlib import Path
-from typing import List, Tuple
import pytest
from craft_parts.overlays import overlays
-
from rockcraft import errors, layers
-def get_tar_contents(tar_path: Path) -> List[str]:
+def get_tar_contents(tar_path: Path) -> list[str]:
with tarfile.open(tar_path, "r") as tar_file:
return tar_file.getnames()
-def duplicate_dirs_setup(tmp_path) -> Tuple[Path, Path]:
+def duplicate_dirs_setup(tmp_path) -> tuple[Path, Path]:
"""Create a filetree with an upper layer and a fake 'rootfs' structure.
layer_dir/
diff --git a/tests/unit/test_oci.py b/tests/unit/test_oci.py
index 1e84cee87..c0541fa3f 100644
--- a/tests/unit/test_oci.py
+++ b/tests/unit/test_oci.py
@@ -23,11 +23,11 @@
from unittest.mock import ANY, call, mock_open, patch
import pytest
-
-import tests
from rockcraft import errors, oci
from rockcraft.architectures import SUPPORTED_ARCHS
+import tests
+
MOCK_NEW_USER = {
"user": "foo",
"uid": 585287,
@@ -41,54 +41,54 @@
}
-@pytest.fixture
+@pytest.fixture()
def mock_run(mocker):
- yield mocker.patch("rockcraft.oci._process_run")
+ return mocker.patch("rockcraft.oci._process_run")
-@pytest.fixture
+@pytest.fixture()
def mock_archive_layer(mocker):
- yield mocker.patch("rockcraft.layers.archive_layer")
+ return mocker.patch("rockcraft.layers.archive_layer")
-@pytest.fixture
+@pytest.fixture()
def mock_rmtree(mocker):
- yield mocker.patch("shutil.rmtree")
+ return mocker.patch("shutil.rmtree")
-@pytest.fixture
+@pytest.fixture()
def mock_mkdir(mocker):
- yield mocker.patch("pathlib.Path.mkdir")
+ return mocker.patch("pathlib.Path.mkdir")
-@pytest.fixture
+@pytest.fixture()
def mock_mkdtemp(mocker):
- yield mocker.patch("tempfile.mkdtemp")
+ return mocker.patch("tempfile.mkdtemp")
-@pytest.fixture
+@pytest.fixture()
def mock_tmpdir(mocker):
- yield mocker.patch("tempfile.TemporaryDirectory")
+ return mocker.patch("tempfile.TemporaryDirectory")
-@pytest.fixture
+@pytest.fixture()
def mock_inject_variant(mocker):
- yield mocker.patch("rockcraft.oci._inject_architecture_variant")
+ return mocker.patch("rockcraft.oci._inject_architecture_variant")
-@pytest.fixture
+@pytest.fixture()
def mock_read_bytes(mocker):
- yield mocker.patch("pathlib.Path.read_bytes")
+ return mocker.patch("pathlib.Path.read_bytes")
-@pytest.fixture
+@pytest.fixture()
def mock_write_bytes(mocker):
- yield mocker.patch("pathlib.Path.write_bytes")
+ return mocker.patch("pathlib.Path.write_bytes")
-@pytest.fixture
+@pytest.fixture()
def mock_add_layer(mocker):
- yield mocker.patch("rockcraft.oci.Image.add_layer")
+ return mocker.patch("rockcraft.oci.Image.add_layer")
@tests.linux_only
@@ -164,7 +164,7 @@ def _get_arch_from_call(self, mock_call):
# The archs here were taken from the supported architectures in the registry
# that we currently use (https://gallery.ecr.aws/ubuntu/ubuntu)
@pytest.mark.parametrize(
- ["deb_arch", "expected_arch", "expected_variant"],
+ ("deb_arch", "expected_arch", "expected_variant"),
[
("amd64", "amd64", None),
("arm64", "arm64", "v8"),
@@ -301,7 +301,7 @@ def test_add_layer(self, mocker, mock_run, new_dir):
"tag",
]
assert mock_run.mock_calls == [
- call(expected_cmd + ["--history.created_by", " ".join(expected_cmd)])
+ call([*expected_cmd, "--history.created_by", " ".join(expected_cmd)])
]
def test_add_new_user(
@@ -361,7 +361,12 @@ def test_add_new_user(
check.is_in("conflict with existing user/group in the base filesystem", err)
@pytest.mark.parametrize(
- "base_user_files,prime_user_files,whiteouts_exist,expected_user_files",
+ (
+ "base_user_files",
+ "prime_user_files",
+ "whiteouts_exist",
+ "expected_user_files",
+ ),
[
# If file in prime, the rest doesn't matter
(
@@ -645,7 +650,7 @@ def test_set_cmd_nonempty2(self, mock_run):
]
@pytest.mark.parametrize(
- "mock_services,mock_checks",
+ ("mock_services", "mock_checks"),
[
# Both services and checks are given
(
@@ -769,9 +774,9 @@ def test_set_control_data(
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
metadata = {"name": "rock-name", "version": 1, "created": now}
- expected = (
- f"created: '{now}'" + "{n}" "name: rock-name{n}" "version: 1{n}"
- ).format(n=os.linesep)
+ expected = (f"created: '{now}'" + "{n}name: rock-name{n}version: 1{n}").format(
+ n=os.linesep
+ )
mocked_data = {"writes": ""}
@@ -797,12 +802,12 @@ def mock_write(s):
"raw",
"add-layer",
"--image",
- str("/c/a:b"),
+ "/c/a:b",
str(f"/c/.temp_layer.control_data.{os.getpid()}.tar"),
]
assert mock_run.mock_calls == [
call(
- expected_cmd + ["--history.created_by", " ".join(expected_cmd)],
+ [*expected_cmd, "--history.created_by", " ".join(expected_cmd)],
)
]
mock_rmtree.assert_called_once_with(Path(mock_control_data_path))
@@ -829,7 +834,7 @@ def test_set_annotations(self, mocker):
],
capture_output=True,
check=True,
- universal_newlines=True,
+ text=True,
),
call(
[
@@ -845,7 +850,7 @@ def test_set_annotations(self, mocker):
],
capture_output=True,
check=True,
- universal_newlines=True,
+ text=True,
),
]
diff --git a/tests/unit/test_pebble.py b/tests/unit/test_pebble.py
index 1e7f98f32..c10bc92a3 100644
--- a/tests/unit/test_pebble.py
+++ b/tests/unit/test_pebble.py
@@ -20,11 +20,11 @@
import pydantic
import pytest
import yaml
-
-import tests
from rockcraft.models.project import ProjectValidationError
from rockcraft.pebble import Check, ExecCheck, HttpCheck, Pebble, Service, TcpCheck
+import tests
+
@tests.linux_only
class TestPebble:
@@ -40,7 +40,12 @@ def test_attributes(self):
)
@pytest.mark.parametrize(
- "existing_layers,expected_new_layer_prefix,layer_content,expected_layer_yaml",
+ (
+ "existing_layers",
+ "expected_new_layer_prefix",
+ "layer_content",
+ "expected_layer_yaml",
+ ),
[
# Test Case 1:
# Without any previous layers, the default layer prefix is 001.
@@ -60,20 +65,20 @@ def test_attributes(self):
},
(
"summary: mock summary"
- "{n}"
+ f"{os.linesep}"
"description: mock description"
- "{n}"
+ f"{os.linesep}"
"services:"
- "{n}"
+ f"{os.linesep}"
" mockServiceOne:"
- "{n}"
+ f"{os.linesep}"
" override: replace"
- "{n}"
+ f"{os.linesep}"
" command: foo"
- "{n}"
+ f"{os.linesep}"
" on-success: shutdown"
- "{n}"
- ).format(n=os.linesep),
+ f"{os.linesep}"
+ ),
),
# Test Case 2:
# With existing layers, the default layer prefix is an increment.
@@ -91,24 +96,24 @@ def test_attributes(self):
},
(
"summary: mock summary"
- "{n}"
+ f"{os.linesep}"
"description: mock description"
- "{n}"
+ f"{os.linesep}"
"services:"
- "{n}"
+ f"{os.linesep}"
" mockServiceOne:"
- "{n}"
+ f"{os.linesep}"
" override: replace"
- "{n}"
+ f"{os.linesep}"
" command: foo"
- "{n}"
+ f"{os.linesep}"
" mockServiceTwo:"
- "{n}"
+ f"{os.linesep}"
" override: merge"
- "{n}"
+ f"{os.linesep}"
" command: bar"
- "{n}"
- ).format(n=os.linesep),
+ f"{os.linesep}"
+ ),
),
# Test Case 3:
# If there are more files that are not layers, they are ignored.
@@ -124,18 +129,18 @@ def test_attributes(self):
},
(
"summary: mock summary"
- "{n}"
+ f"{os.linesep}"
"description: mock description"
- "{n}"
+ f"{os.linesep}"
"services:"
- "{n}"
+ f"{os.linesep}"
" mockServiceOne:"
- "{n}"
+ f"{os.linesep}"
" override: replace"
- "{n}"
+ f"{os.linesep}"
" command: foo"
- "{n}"
- ).format(n=os.linesep),
+ f"{os.linesep}"
+ ),
),
],
)
@@ -221,7 +226,7 @@ def test_full_service(self, service):
_ = Service(**service)
@pytest.mark.parametrize(
- "bad_service,error",
+ ("bad_service", "error"),
[
# Missing fields
({}, r"^2 validation errors[\s\S]*override[\s\S]*command"),
@@ -274,7 +279,7 @@ def test_bad_services(self, bad_service, error):
_ = Service(**bad_service)
@pytest.mark.parametrize(
- "bad_http_check,error",
+ ("bad_http_check", "error"),
[
# Missing fields
({}, r"^1 validation error[\s\S]*url[\s\S]"),
@@ -295,7 +300,7 @@ def test_bad_http_checks(self, bad_http_check, error):
_ = HttpCheck(**bad_http_check)
@pytest.mark.parametrize(
- "bad_tcp_check,error",
+ ("bad_tcp_check", "error"),
[
# Missing fields
({}, r"^1 validation error[\s\S]*port[\s\S]"),
@@ -316,7 +321,7 @@ def test_bad_tcp_checks(self, bad_tcp_check, error):
_ = TcpCheck(**bad_tcp_check)
@pytest.mark.parametrize(
- "bad_exec_check,error",
+ ("bad_exec_check", "error"),
[
# Missing fields
({}, r"^1 validation error[\s\S]*command[\s\S]"),
@@ -363,7 +368,7 @@ def test_minimal_check(self):
_ = Check(override="merge", exec={"command": "foo cmd"}) # pyright: ignore
@pytest.mark.parametrize(
- "bad_check,exception,error",
+ ("bad_check", "exception", "error"),
[
# Missing check type fields
(
diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py
index 27ddb061b..dd073cb07 100644
--- a/tests/unit/test_project.py
+++ b/tests/unit/test_project.py
@@ -19,14 +19,13 @@
import subprocess
import textwrap
from pathlib import Path
-from typing import Any, Dict
+from typing import Any
import pydantic
import pytest
import yaml
from craft_application.models import BuildInfo
from craft_providers.bases import BaseName
-
from rockcraft.errors import ProjectLoadError, ProjectValidationError
from rockcraft.models import Project, load_project
from rockcraft.models.project import INVALID_NAME_MESSAGE, Platform
@@ -87,18 +86,18 @@
"""
-@pytest.fixture
+@pytest.fixture()
def yaml_data():
return ROCKCRAFT_YAML
-@pytest.fixture
+@pytest.fixture()
def yaml_loaded_data():
return yaml.safe_load(ROCKCRAFT_YAML)
-@pytest.fixture
-def pebble_part() -> Dict[str, Any]:
+@pytest.fixture()
+def pebble_part() -> dict[str, Any]:
return {
"pebble": {
"plugin": "nil",
@@ -181,7 +180,7 @@ def test_project_unmarshal_with_unsupported_fields(unsupported_field, yaml_loade
@pytest.mark.parametrize(
- "variable,is_forbidden",
+ ("variable", "is_forbidden"),
[("$BAR", True), ("BAR_$BAZ", True), ("BAR$", False)],
)
def test_forbidden_env_var_interpolation(
@@ -272,9 +271,9 @@ def test_project_entrypoint_service_valid(
assert project.entrypoint_service == entrypoint_service
emitter.assert_message(
"Warning: defining an entrypoint-service will result in a rock with "
- + "an atypical OCI Entrypoint. While that might be acceptable for "
- + "testing and personal use, it shall require prior approval before "
- + "submitting to a Canonical registry namespace."
+ "an atypical OCI Entrypoint. While that might be acceptable for "
+ "testing and personal use, it shall require prior approval before "
+ "submitting to a Canonical registry namespace."
)
@@ -304,7 +303,7 @@ def test_project_build_base(yaml_loaded_data):
@pytest.mark.parametrize(
- ["base", "build_base", "expected_base", "expected_build_base"],
+ ("base", "build_base", "expected_base", "expected_build_base"),
[
("ubuntu:22.04", None, "ubuntu@22.04", "ubuntu@22.04"),
("ubuntu:22.04", "ubuntu:20.04", "ubuntu@22.04", "ubuntu@20.04"),
@@ -457,7 +456,7 @@ def test_project_parts_validation(yaml_loaded_data):
@pytest.mark.parametrize(
- "packages,script",
+ ("packages", "script"),
[
(["pkg"], None),
([], "ls"),
@@ -613,7 +612,7 @@ def test_project_yaml(yaml_loaded_data):
@pytest.mark.parametrize(
- ["platforms", "expected_build_infos"],
+ ("platforms", "expected_build_infos"),
[
(
{
diff --git a/tests/unit/test_usernames.py b/tests/unit/test_usernames.py
index c279c9e72..d5cd2c105 100644
--- a/tests/unit/test_usernames.py
+++ b/tests/unit/test_usernames.py
@@ -16,7 +16,6 @@
import pydantic
import pytest
-
from rockcraft import usernames
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index d36d3c2db..f77eb4d00 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -18,23 +18,22 @@
from unittest.mock import call
import pytest
-
from rockcraft import utils
-@pytest.fixture
+@pytest.fixture()
def mock_isatty(mocker):
- yield mocker.patch("rockcraft.utils.sys.stdin.isatty", return_value=True)
+ return mocker.patch("rockcraft.utils.sys.stdin.isatty", return_value=True)
-@pytest.fixture
+@pytest.fixture()
def mock_input(mocker):
- yield mocker.patch("rockcraft.utils.input", return_value="")
+ return mocker.patch("rockcraft.utils.input", return_value="")
-@pytest.fixture
+@pytest.fixture()
def mock_is_managed_mode(mocker):
- yield mocker.patch("rockcraft.utils.is_managed_mode", return_value=False)
+ return mocker.patch("rockcraft.utils.is_managed_mode", return_value=False)
def test_get_managed_environment_home_path():
@@ -83,7 +82,7 @@ def test_confirm_with_user_defaults_without_tty(mock_input, mock_isatty):
@pytest.mark.parametrize(
- "user_input,expected",
+ ("user_input", "expected"),
[
("y", True),
("Y", True),
diff --git a/tests/unit/testing/extensions.py b/tests/unit/testing/extensions.py
index 1b4561379..fed18152b 100644
--- a/tests/unit/testing/extensions.py
+++ b/tests/unit/testing/extensions.py
@@ -16,10 +16,9 @@
"""Fake Extensions for use in tests."""
import textwrap
-from typing import Any, Dict, Optional, Tuple
+from typing import Any
from overrides import override
-
from rockcraft.extensions.extension import Extension
@@ -29,24 +28,24 @@ class FakeExtension(Extension):
NAME = "fake-extension"
@staticmethod
- def get_supported_bases() -> Tuple[str, ...]:
+ def get_supported_bases() -> tuple[str, ...]:
"""Return a tuple of supported bases."""
return ("ubuntu@22.04",)
@staticmethod
- def is_experimental(base: Optional[str]) -> bool:
+ def is_experimental(base: str | None) -> bool:
"""Return whether or not this extension is unstable for given base."""
return False
- def get_root_snippet(self) -> Dict[str, Any]:
+ def get_root_snippet(self) -> dict[str, Any]:
"""Return the root snippet to apply."""
return {}
- def get_part_snippet(self) -> Dict[str, Any]:
+ def get_part_snippet(self) -> dict[str, Any]:
"""Return the part snippet to apply to existing parts."""
return {}
- def get_parts_snippet(self) -> Dict[str, Any]:
+ def get_parts_snippet(self) -> dict[str, Any]:
"""Return the parts to add to parts."""
return {}
@@ -57,12 +56,12 @@ class ExperimentalExtension(FakeExtension):
NAME = "experimental-extension"
@staticmethod
- def get_supported_bases() -> Tuple[str, ...]:
+ def get_supported_bases() -> tuple[str, ...]:
"""Return a tuple of supported bases."""
return ("ubuntu@22.04", "ubuntu@20.04")
@staticmethod
- def is_experimental(base: Optional[str]) -> bool:
+ def is_experimental(base: str | None) -> bool:
return True
@@ -72,7 +71,7 @@ class InvalidPartExtension(FakeExtension):
NAME = "invalid-extension"
@override
- def get_parts_snippet(self) -> Dict[str, Any]:
+ def get_parts_snippet(self) -> dict[str, Any]:
return {"bad-name": {"plugin": "dump", "source": None}}
@@ -82,7 +81,7 @@ class FullExtension(FakeExtension):
NAME = "full-extension"
@override
- def get_root_snippet(self) -> Dict[str, Any]:
+ def get_root_snippet(self) -> dict[str, Any]:
"""Return the root snippet to apply."""
return {
"services": {
@@ -94,12 +93,12 @@ def get_root_snippet(self) -> Dict[str, Any]:
}
@override
- def get_part_snippet(self) -> Dict[str, Any]:
+ def get_part_snippet(self) -> dict[str, Any]:
"""Return the part snippet to apply to existing parts."""
return {"stage-packages": ["new-package-1"]}
@override
- def get_parts_snippet(self) -> Dict[str, Any]:
+ def get_parts_snippet(self) -> dict[str, Any]:
"""Return the parts to add to parts."""
return {"full-extension/new-part": {"plugin": "nil", "source": None}}
diff --git a/tox.ini b/tox.ini
index 5072c7cf9..038dfaebb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,13 @@
[tox]
env_list = # Environments to run when called with no parameters.
-minversion = 4.3.5
+ format-{black,ruff,codespell}
+ pre-commit
+ lint-{black,ruff,mypy,pyright,shellcheck,codespell,docs,yaml}
+ unit-py3.{10,11}
+ integration-py3.10
+# Integration tests probably take a while, so we're only running them on Python
+# 3.10, which is included in core22.
+minversion = 4.6
# Tox will use these requirements to bootstrap a venv if necessary.
# tox-igore-env-name-mismatch allows us to have one virtualenv for all linting.
# By setting requirements here, we make this INI file compatible with older
@@ -9,7 +16,9 @@ minversion = 4.3.5
# install tox from apt. Older than that, the user gets an upgrade warning.
requires =
# renovate: datasource=pypi
- tox-ignore-env-name-mismatch==0.2.0.post2
+ tox-ignore-env-name-mismatch>=0.2.0.post2
+ # renovate: datasource=pypi
+ tox-gh==1.3.1
# Allow tox to access the user's $TMPDIR environment variable if set.
# This workaround is required to avoid circular dependencies for TMPDIR,
# since tox will otherwise attempt to use the environment's TMPDIR variable.
@@ -23,38 +32,120 @@ env_tmp_dir = {user_tmp_dir:{env:XDG_RUNTIME_DIR:{work_dir}}}/tox_tmp/{env_name}
set_env =
TMPDIR={env_tmp_dir}
COVERAGE_FILE={env_tmp_dir}/.coverage_{env_name}
+pass_env =
+ CI
+ CRAFT_*
+ PYTEST_ADDOPTS
+ RUNNER_ARCH
+
+[test] # Base configuration for unit and integration tests
+package = editable
+extras = dev, ubuntu-jammy
+allowlist_externals = mkdir
+commands_pre = mkdir -p {tox_root}/results
+
+[testenv:{unit,integration}-py3.{10,11,12}] # Configuration for all tests using pytest
+base = testenv, test
+description =
+ unit: Run unit tests with pytest
+ integration: Run integration tests with pytest
+labels =
+ py3.{10,11}: tests
+ unit-py3.{10,11}: unit-tests
+ integration-py3.{10,11}: integration-tests
+change_dir =
+ unit: tests/unit
+ integration: tests/integration
+commands = pytest {tty:--color=yes} --cov={tox_root}/rockcraft --cov-config={tox_root}/pyproject.toml --cov-report=xml:{tox_root}/results/coverage-{env_name}.xml --junit-xml={tox_root}/results/test-results-{env_name}.xml -m "not notox" {posargs}
+
+[lint] # Standard linting configuration
+package = editable
+extras = lint
+env_dir = {work_dir}/linting
+runner = ignore_env_name_mismatch
+
+[shellcheck]
+find = git ls-files
+filter = file --mime-type -Nnf- | grep shellscript | cut -f1 -d:
+
+[testenv:lint-{black,ruff,shellcheck,codespell,yaml}]
+description = Lint the source code
+base = testenv, lint
+labels = lint
+allowlist_externals =
+ shellcheck: bash, xargs
+commands_pre =
+ shellcheck: bash -c '{[shellcheck]find} | {[shellcheck]filter} > {env_tmp_dir}/shellcheck_files'
+commands =
+ black: black --check --diff {tty:--color} {posargs} .
+ ruff: ruff check --respect-gitignore {posargs:.}
+ shellcheck: xargs -ra {env_tmp_dir}/shellcheck_files shellcheck
+ codespell: codespell --toml {tox_root}/pyproject.toml {posargs}
+ yaml: yamllint {posargs} .
+
+[testenv:lint-{mypy,pyright}]
+description = Static type checking
+base = testenv, lint
+env_dir = {work_dir}/typing
+extras = dev, types
+labels = lint, type
+allowlist_externals =
+ mypy: mkdir
+commands_pre =
+ mypy: mkdir -p {tox_root}/.mypy_cache
+commands =
+ pyright: pyright {posargs}
+ mypy: mypy --install-types --non-interactive {posargs:.}
+
+[testenv:format-{black,ruff,codespell}]
+description = Automatically format source code
+base = testenv, lint
+labels = format
+commands =
+ black: black {tty:--color} {posargs} .
+ ruff: ruff --fix --respect-gitignore {posargs} .
+ codespell: codespell --toml {tox_root}/pyproject.toml --write-changes {posargs}
+
+[testenv:pre-commit]
+base =
+deps = pre-commit
+package = skip
+no_package = true
+env_dir = {work_dir}/pre-commit
+runner = ignore_env_name_mismatch
+description = Run pre-commit on staged files or arbitrary pre-commit commands (tox run -e pre-commit -- [args])
+commands = pre-commit {posargs:run}
[docs] # Sphinx documentation configuration
-deps = -r requirements-jammy.txt
-extras = doc
+extras = docs
package = editable
no_package = true
env_dir = {work_dir}/docs
runner = ignore_env_name_mismatch
-allowlist_externals = bash
-commands_pre =
- bash -c 'if [[ ! -e docs ]];then echo "No docs directory. Run `tox run -e sphinx-quickstart` to create one.";return 1;fi'
-
-[testenv:link-docs-pkg]
-description =
- Use a local (editable) dependency package for documentation rather than the version in requirements.
- To run: `tox run -e link-docs-pkg -- [repo_directory]`
-base = docs
-commands = pip install -e {posargs}
+source_dir = {tox_root}/{project_name}
[testenv:build-docs]
description = Build sphinx documentation
base = docs
+allowlist_externals = bash
+commands_pre = bash -c 'if [[ ! -e docs ]];then echo "No docs directory. Run `tox run -e sphinx-quickstart` to create one.;";return 1;fi'
# "-W" is to treat warnings as errors
-commands = sphinx-build {posargs:-b html} {tox_root}/docs {tox_root}/docs/_build
+commands = sphinx-build {posargs:-b html} -W {tox_root}/docs {tox_root}/docs/_build
[testenv:autobuild-docs]
description = Build documentation with an autoupdating server
base = docs
-commands = sphinx-autobuild {posargs:-b html --open-browser --port 8080} --watch {tox_root}/rockcraft {tox_root}/docs {tox_root}/docs/_build
+commands = sphinx-autobuild {posargs:-b html --open-browser --port 8080} -W --watch {source_dir} {tox_root}/docs {tox_root}/docs/_build
+
+[lint-docs]
+find = git ls-files
[testenv:lint-docs]
description = Lint the documentation with sphinx-lint
base = docs
-commands = sphinx-lint --ignore docs/_build -e all {posargs} docs/
-labels = lint
\ No newline at end of file
+labels = lint
+allowlist_externals = bash, xargs
+commands_pre = bash -c '{[lint-docs]find} > {env_tmp_dir}/lint_docs_files'
+commands =
+ xargs --no-run-if-empty --arg-file {env_tmp_dir}/lint_docs_files sphinx-lint --max-line-length 80 --enable all --ignore "docs/sphinx-starter-pack/" --ignore "README.rst" {posargs}
+ sphinx-lint --max-line-length 100 --enable all "README.rst" {posargs}
\ No newline at end of file