diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6056a3e --- /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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 440f2ad..853a67b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,3 @@ +- [ ] Have you followed the guidelines for contributing? - [ ] Have you signed the [CLA](http://www.ubuntu.com/legal/contributors/)? - ------ +- [ ] Have you successfully run `tox`? diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 472a2a0..5262e23 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -15,9 +15,7 @@ categories: - title: "Tooling" label: - "tooling" - change-template: '- $TITLE @$AUTHOR (#$NUMBER)' - template: | Special thanks to the contributors that made this release happen: $CONTRIBUTORS diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000..fa9af8d --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,101 @@ +{ + // Configuration file for RenovateBot: https://docs.renovatebot.com/configuration-options + extends: ["config:base"], + labels: ["dependencies"], // For convenient searching in GitHub + pip_requirements: { + fileMatch: ["^tox.ini$", "(^|/)requirements([\\w-]*)\\.txt$"] + }, + packageRules: [ + { + // Automerge patches, pin changes and digest changes. + // Also groups these changes together. + groupName: "bugfixes", + excludePackagePrefixes: ["dev", "lint", "types"], + matchUpdateTypes: ["patch", "pin", "digest"], + prPriority: 3, // Patches should go first! + automerge: true + }, + { + // Update all internal packages in one higher-priority PR + groupName: "internal packages", + matchPackagePrefixes: ["craft-", "snap-"], + matchLanguages: ["python"], + prPriority: 2 + }, + { + // GitHub Actions are higher priority to update than most dependencies. + groupName: "GitHub Actions", + matchManagers: ["github-actions"], + prPriority: 1, + automerge: true, + }, + // Everything not in one of these rules gets priority 0 and falls here. + { + // Minor changes can be grouped and automerged for dev dependencies, but are also deprioritised. + groupName: "development dependencies (non-major)", + groupSlug: "dev-dependencies", + matchPackagePrefixes: [ + "dev", + "lint", + "types" + ], + excludePackagePatterns: ["ruff"], + matchUpdateTypes: ["minor", "patch", "pin", "digest"], + prPriority: -1, + automerge: true + }, + { + // Documentation related updates + groupName: "documentation dependencies", + groupSlug: "doc-dependencies", + matchPackageNames: ["Sphinx"], + matchPackagePatterns: ["^[Ss]phinx.*$", "^furo$"], + matchPackagePrefixes: ["docs"], + }, + { + // Other major dependencies get deprioritised below minor dev dependencies. + matchUpdateTypes: ["major"], + prPriority: -2 + }, + { + // Major dev dependencies are stone last, but grouped. + groupName: "development dependencies (major versions)", + groupSlug: "dev-dependencies", + matchDepTypes: ["devDependencies"], + matchUpdateTypes: ["major"], + prPriority: -3 + }, + { + // Ruff is still unstable, so update it separately. + groupName: "ruff", + matchPackagePatterns: ["^(lint/)?ruff$"], + prPriority: -3 + } + ], + regexManagers: [ + { + // tox.ini can get updates too if we specify for each package. + fileMatch: ["tox.ini"], + depTypeTemplate: "devDependencies", + matchStrings: [ + "# renovate: datasource=(?\\S+)\n\\s+(?.*?)(\\[[\\w]*\\])*[=><]=?(?.*?)\n" + ] + }, + { + // .pre-commit-config.yaml version updates + fileMatch: [".pre-commit-config.yaml"], + depTypeTemplate: "devDependencies", + matchStrings: [ + "# renovate: datasource=(?\\S+);\\s*depName=(?.*?)\n\s+rev: \"v?(?.*?)\"" + ] + } + ], + timezone: "Etc/UTC", + automergeSchedule: "every weekend", + schedule: "every weekend", + prConcurrentLimit: 2, // No more than 2 open PRs at a time. + prCreation: "not-pending", // Wait until status checks have completed before raising the PR + prNotPendingHours: 4, // ...unless the status checks have been running for 4+ hours. + prHourlyLimit: 1, // No more than 1 PR per hour. + stabilityDays: 2 // Wait 2 days from release before updating. +} diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 612d89a..cdb271a 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -3,7 +3,7 @@ on: [pull_request] jobs: cla-check: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Check if CLA signed uses: canonical/has-signed-canonical-cla@v1 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..4026a0d --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,37 @@ +name: Documentation +on: + push: + branches: + - "main" + - "feature/*" + - "hotfix/*" + - "release/*" + pull_request: + paths: + - "docs/**" + - "pyproject.toml" + - ".github/workflows/docs.yaml" + +jobs: + sphinx: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install Tox + run: pip install tox + - name: Lint documentation + run: tox run -e lint-docs + - name: Build documentation + run: tox run -e build-docs + - name: Upload documentation + uses: actions/upload-artifact@v3 + with: + name: docs + path: docs/_build/ diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yaml similarity index 65% rename from .github/workflows/release-drafter.yml rename to .github/workflows/release-drafter.yaml index cf1f1f9..e60ebc1 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yaml @@ -2,6 +2,7 @@ name: Release Drafter on: push: + # branches to consider in the event; optional, defaults to all branches: - main @@ -10,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Release Drafter - uses: release-drafter/release-drafter@v5.7.0 + uses: release-drafter/release-drafter@v5.23.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e0239ff..f1e3039 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,75 +1,79 @@ -name: Tests - +name: Tests, linting, etc. on: - pull_request: push: branches: - - main + - "main" + - "feature/*" + - "hotfix/*" + - "release/*" + pull_request: jobs: linters: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - name: Setup Python + uses: actions/setup-python@v4 with: - python-version: "3.10" - - name: Install python packages and dependencies - run: | - pip install -U -e .[dev] - - 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 + python-version: '3.10' + - name: Configure environment run: | - make test-pydocstyle - - name: Run pylint - run: | - make test-pylint - - name: Run pyright - run: | - sudo snap install --classic node - sudo snap install --classic pyright - make test-pyright - + 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>=4' tox-gh + echo "::endgroup::" + echo "::group::Create virtual environments for linting processes." + tox run -m lint --notest + echo "::endgroup::" + echo "::group::Wait for snap to complete" + snap watch --last=install + echo "::endgroup::" + - name: Run Linters + run: tox run -m lint tests: strategy: matrix: - os: [macos-11, macos-12, ubuntu-20.04, ubuntu-22.04, windows-2019, windows-2022] - python-version: [3.8, 3.9, "3.10", "3.11", "3.12-dev"] - - runs-on: ${{ matrix.os }} + platform: [macos-latest, ubuntu-20.04, ubuntu-22.04, windows-latest] + runs-on: ${{ matrix.platform }} steps: - - name: Checkout code - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v2 + - name: Set up Python versions on ${{ matrix.platform }} + uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} - - name: Install generic dependencies and craft-cli + python-version: | + 3.8 + 3.9 + 3.10 + 3.11 + 3.12-dev + - name: Configure environment run: | - pip install -U -e .[dev] - - name: Run unit tests - run: | - make test-units - - name: Run integration tests - run: | - make test-integrations + echo "::group::pip install" + python -m pip install 'tox>=4' tox-gh + echo "::endgroup::" + mkdir -p results + - name: Setup Tox environments + run: tox run -m tests --notest + - name: Test with tox + run: tox run-parallel --parallel all --parallel-no-spinner --skip-pkg-install --result-json results/tox-${{ matrix.platform }}.json -m tests -- --no-header --quiet -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/ diff --git a/.gitignore b/.gitignore index b4b993e..b19a46a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -21,9 +20,12 @@ parts/ sdist/ var/ wheels/ +pip-wheel-metadata/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -38,12 +40,14 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ .pytest_cache/ @@ -54,6 +58,8 @@ coverage.xml # Django stuff: *.log local_settings.py +db.sqlite3 +db.sqlite3-journal # Flask stuff: instance/ @@ -71,22 +77,38 @@ target/ # Jupyter Notebook .ipynb_checkpoints +# IPython +profile_default/ +ipython_config.py + # pyenv .python-version -# celery beat schedule file +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid # SageMath parsed files *.sage.py -# dotenv +# Environments .env - -# virtualenv .venv +env/ venv/ ENV/ +env.bak/ +venv.bak/ # Spyder project settings .spyderproject @@ -100,10 +122,20 @@ ENV/ # mypy .mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ -# IDE settings -.vscode/ +# Caches for various tools +/.*_cache/ + +# Test results +/results/ # direnv -.direnv .envrc + +# Ignore version module generated by setuptools_scm +/*/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..66c282e --- /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.0.269" + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - repo: https://github.com/psf/black + # renovate: datasource=pypi;depName=black + rev: "23.3.0" + hooks: + - id: black + - repo: https://github.com/adrienverge/yamllint.git + # renovate: datasource=pypi;depName=yamllint + rev: "v1.32.0" + hooks: + - id: yamllint diff --git a/.pylintrc b/.pylintrc index ad4e341..abb4766 100644 --- a/.pylintrc +++ b/.pylintrc @@ -547,5 +547,5 @@ preferred-modules= # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 51% rename from .readthedocs.yml rename to .readthedocs.yaml index efed0d7..fb58117 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -1,22 +1,27 @@ -# .readthedocs.yml +# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# Required version: 2 +# Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py -build: - os: "ubuntu-20.04" - tools: - python: "3.8" - +# Optionally build your docs in additional formats such as PDF formats: - pdf + - epub + +build: + os: ubuntu-22.04 + tools: + python: "3" python: install: - - requirements: docs/requirements.txt - method: pip path: . + extra_requirements: + - docs diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..8f7da74 --- /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 4f86db6..0000000 --- a/Makefile +++ /dev/null @@ -1,106 +0,0 @@ -SOURCES=$(wildcard *.py) craft_cli tests - -.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 $(SOURCES) - autoflake --remove-all-unused-imports --ignore-init-module-imports -ri $(SOURCES) - black $(SOURCES) - -.PHONY: clean -clean: ## Clean artifacts 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/craft_cli.* - rm -f docs/modules.rst - 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 - -.PHONY: coverage -coverage: ## Run pytest with coverage report. - coverage run --source craft_sore -m pytest - coverage report -m - coverage html - -.PHONY: docs -docs: ## Generate documentation. - rm -f docs/craft_cli.rst - rm -f docs/modules.rst - $(MAKE) -C docs clean - $(MAKE) -C docs html - -.PHONY: dist -dist: clean ## Build python package. - python setup.py sdist - python setup.py bdist_wheel - ls -l dist - -.PHONY: install -install: clean ## Install python package. - pip install - -.PHONY: lint -lint: test-black test-codespell test-flake8 test-isort test-mypy test-pydocstyle test-pylint test-pyright ## Run all linting tests. - -.PHONY: release -release: dist ## Release with twine. - twine upload dist/* - -.PHONY: test-black -test-black: - black --check --diff $(SOURCES) - -.PHONY: test-codespell -test-codespell: - codespell $(SOURCES) - -.PHONY: test-flake8 -test-flake8: - flake8 $(SOURCES) - -.PHONY: test-integrations -test-integrations: ## Run integration tests. - pytest tests/integration - -.PHONY: test-isort -test-isort: - isort --check $(SOURCES) - -.PHONY: test-mypy -test-mypy: - mypy $(SOURCES) - -.PHONY: test-pydocstyle -test-pydocstyle: - pydocstyle craft_cli - -.PHONY: test-pylint -test-pylint: - pylint craft_cli - pylint tests --disable=missing-module-docstring,missing-function-docstring,redefined-outer-name,protected-access,duplicate-code,too-many-lines,missing-class-docstring,too-few-public-methods - -.PHONY: test-pyright -test-pyright: - pyright $(SOURCES) - -.PHONY: test-units -test-units: ## Run unit tests. - pytest tests/unit - -.PHONY: tests -tests: lint test-integrations test-units ## Run all tests. diff --git a/craft_cli/__init__.py b/craft_cli/__init__.py index d5b3cab..f8840e8 100644 --- a/craft_cli/__init__.py +++ b/craft_cli/__init__.py @@ -16,7 +16,16 @@ """Interact with Canonical services such as Charmhub and the Snap Store.""" -__version__ = "1.2.0" +try: + from ._version import __version__ +except ImportError: # pragma: no cover + from importlib.metadata import version, PackageNotFoundError + + try: + __version__ = version("craft-cli") + except PackageNotFoundError: + __version__ = "dev" + # names included here only to be exposed as external API; the particular order of imports # is to break cyclic dependencies diff --git a/craft_cli/dispatcher.py b/craft_cli/dispatcher.py index 7936c51..1c45d61 100644 --- a/craft_cli/dispatcher.py +++ b/craft_cli/dispatcher.py @@ -346,7 +346,9 @@ def _build_no_command_error(self, missing_command: str) -> str: msg = f"no such command {missing_command!r}{extra_similar}" return self._help_builder.get_usage_message(msg) - def _parse_options(self, defined_arguments, sysargs): # pylint: disable=too-many-branches + def _parse_options( + self, defined_arguments: List[GlobalArgument], sysargs: List[str] + ): # pylint: disable=too-many-branches """Parse arguments.""" # get all arguments (default to what's specified) and those per options, to filter sysargs global_args: Dict[str, Any] = {} diff --git a/craft_cli/messages.py b/craft_cli/messages.py index 2c391a0..4b5a167 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -31,7 +31,7 @@ import traceback from contextlib import contextmanager from datetime import datetime -from typing import Dict, Literal, Optional, TextIO, Union +from typing import Any, Callable, cast, Dict, Literal, Optional, TextIO, TypeVar, Union import platformdirs @@ -351,7 +351,10 @@ def emit(self, record: logging.LogRecord) -> None: self.printer.show(stream, record.getMessage(), use_timestamp=use_timestamp) -def _active_guard(ignore_when_stopped=False): +FuncT = TypeVar("FuncT", bound=Callable[..., Any]) + + +def _active_guard(ignore_when_stopped: bool = False) -> Callable[..., Any]: """Decorate Emitter methods to be called when active. It will check that the emitter is initiated and that is not stopped (except when @@ -359,8 +362,10 @@ def _active_guard(ignore_when_stopped=False): double-ending). """ - def decorator(wrapped_func): - def func(self, *args, **kwargs): # pylint: disable=inconsistent-return-statements + def decorator(wrapped_func: FuncT) -> FuncT: + def func( # pylint: disable=inconsistent-return-statements + self, *args: Any, **kwargs: Any + ) -> Any: if not self._initiated: # pylint: disable=protected-access raise RuntimeError("Emitter needs to be initiated first") if self._stopped: # pylint: disable=protected-access @@ -369,7 +374,7 @@ def func(self, *args, **kwargs): # pylint: disable=inconsistent-return-statemen raise RuntimeError("Emitter is stopped already") return wrapped_func(self, *args, **kwargs) - return func + return cast(FuncT, func) return decorator diff --git a/craft_cli/printer.py b/craft_cli/printer.py index f45ab89..825c728 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -25,7 +25,7 @@ from dataclasses import dataclass, field from datetime import datetime from functools import lru_cache -from typing import Optional, TextIO, Union +from typing import Any, Optional, TextIO, Union # the char used to draw the progress bar ('FULL BLOCK') _PROGRESS_BAR_SYMBOL = "█" @@ -92,7 +92,7 @@ def __init__(self, printer: "Printer"): self.daemon = True # communication from the printer - self.queue: queue.Queue = queue.Queue() + self.queue: queue.Queue[Any] = queue.Queue() # hold the printer, to make it spin self.printer = printer diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..ef7e97f --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,28 @@ +@import url('https://fonts.googleapis.com/css2?family=Ubuntu:ital@0;1&display=swap'); + +body { + font-family: Ubuntu, "times new roman", times, roman, serif; +} + +div .toctree-wrapper { + column-count: 2; +} + +div .toctree-wrapper>ul { + margin: 0; +} + +ul .toctree-l1 { + margin: 0; + -webkit-column-break-inside: avoid; + page-break-inside: avoid; + break-inside: avoid-column; +} + +.wy-nav-content { + max-width: none; +} + +.log-snippets { + color: rgb(141, 141, 141); +} diff --git a/docs/conf.py b/docs/conf.py index b0066ab..d1734e6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,32 +1,7 @@ -# -# Copyright 2021 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# - # Configuration file for the Sphinx documentation builder. # -# This file only contains a selection of the most common options. For a full -# list see the documentation: +# For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import os import sys @@ -35,64 +10,68 @@ import craft_cli # noqa: E402 - # -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Craft CLI" -copyright = "2021-2022 Canonical Ltd." -author = "Canonical Ltd." +copyright = "2023, Canonical" +author = "Canonical" -# The full version, including alpha/beta/rc tags release = craft_cli.__version__ +# region General configuration +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", "sphinx.ext.viewcode", - "sphinx_autodoc_typehints", # must be loaded after napoleon + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx_design", + "sphinx_copybutton", "sphinx-pydantic", + "sphinx_toolbox", + "sphinx_toolbox.more_autodoc", + "sphinx.ext.autodoc", # Must be loaded after more_autodoc ] -# Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +show_authors = False -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "sphinx_rtd_theme" +# endregion +# region Options for HTML output +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". +html_theme = "furo" html_static_path = ["_static"] +html_css_files = [ + "css/custom.css", +] + +# endregion +# region Options for extensions +# Intersphinx extension +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration -# Do (not) include module names. -add_module_names = True +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} -# sphinx_autodoc_typehints +# Type hints configuration set_type_checking_flag = True typehints_fully_qualified = False always_document_param_types = True typehints_document_rtype = True -# Enable support for google-style instance attributes. -napoleon_use_ivar = True +# Github config +github_username = "canonical" +github_repository = "craft-cli" +# endregion +# region Setup reference generation def run_apidoc(_): from sphinx.ext.apidoc import main import os @@ -101,15 +80,19 @@ def run_apidoc(_): sys.path.append(os.path.join(os.path.dirname(__file__), "..")) cur_dir = os.path.abspath(os.path.dirname(__file__)) module = os.path.join(cur_dir, "..", "craft_cli") - main(["-e", "-o", cur_dir, module, "--no-toc", "--force"]) + exclude_patterns = ["*pytest_plugin*"] + main(["-e", "--no-toc", "--force", "-o", cur_dir, module, *exclude_patterns]) def no_namedtuple_attrib_docstring(app, what, name, obj, options, lines): """Strips out silly "Alias for field number" lines in namedtuples reference.""" - if len(lines) == 1 and lines[0].startswith('Alias for field number'): + if len(lines) == 1 and lines[0].startswith("Alias for field number"): del lines[:] def setup(app): app.connect("builder-inited", run_apidoc) - app.connect('autodoc-process-docstring', no_namedtuple_attrib_docstring) + app.connect("autodoc-process-docstring", no_namedtuple_attrib_docstring) + + +# endregion diff --git a/docs/explanations.rst b/docs/explanations.rst index 4178fb6..c10efcc 100644 --- a/docs/explanations.rst +++ b/docs/explanations.rst @@ -1,9 +1,11 @@ +.. _explanations: + ************ Explanations ************ -About the appropriate mode to initiate `emit` -============================================= +About the appropriate mode to initiate ``emit`` +=============================================== The first mandatory parameter of the ``emit`` object is ``mode``, which controls the initial verboseness level of the system. @@ -37,7 +39,7 @@ Unless overridden when ``emit`` is initiated (see :ref:`how to do that --verbose --verbose -The Dispatcher automatically provides the following global arguments, but more can be specified through the `extra_global_args` option (see :ref:`how to do that `): +The Dispatcher automatically provides the following global arguments, but more can be specified through the ``extra_global_args`` option (see :ref:`how to do that `): - ``-h`` / ``--help``: provides a help text for the application or command - ``-q`` / ``--quiet``: sets the ``emit`` output level to QUIET @@ -58,13 +60,13 @@ Each command can also specify its own arguments parsing rules using the ``fill_p Group of commands ================= -The Dispatcher's `command_groups` parameter is just a list `CommandGroup` objects, each of one grouping different commands for the different types of functionalities that may offer the application. See `its reference here `_, but its use is quite straightforward. E.g.:: +The Dispatcher's ``command_groups`` parameter is just a list ``CommandGroup`` objects, each of one grouping different commands for the different types of functionalities that may offer the application. See `its reference here `_, but its use is quite straightforward. E.g.:: CommandGroup("Basic", [LoginCommand, LogoutCommand]) A list of these command groups is what is passed to the ``Dispatcher`` to run them as part of the application. -This grouping is uniquely for building the help exposed to the user, which improves the UX of the application. +This grouping is uniquely for building the help exposed to the user, which improves the UX of the application. When requesting the full application help, all commands will be grouped and presented in the order declared in each ``CommandGroup`` and in the list given to the ``Dispatcher``, and when requesting help for one command, other commands from the same group are suggested to the user as related to the requested one. @@ -133,7 +135,7 @@ Progress bar The ``progress_bar`` method is to be used in a potentially long-running single step of a command (e.g. a download or provisioning step). -It receives a `text` that should reflect the operation that is about to start, a ``total`` that will be the number to reach when the operation is completed, and optionally a `delta=False` to indicate that calls to ``.advance`` method should pass the total so far (by default is True, which implies that calls to ``.advance`` indicates the delta in the operation progress). Returns a context manager with the ``.advance`` method to call on each progress. +It receives a ``text`` that should reflect the operation that is about to start, a ``total`` that will be the number to reach when the operation is completed, and optionally a ``delta=False`` to indicate that calls to ``.advance`` method should pass the total so far (by default is True, which implies that calls to ``.advance`` indicates the delta in the operation progress). Returns a context manager with the ``.advance`` method to call on each progress. :: @@ -200,7 +202,7 @@ E.g.:: Get messages from subprocesses ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``open_stream`` returns a context manager that can be used to get the standard output and/or error from the executed subprocess. +The ``open_stream`` returns a context manager that can be used to get the standard output and/or error from the executed subprocess. This way all the outputs of the subprocess will be captured by ``craft-cli`` and shown or not to the screen (according to verbosity setup) and always logged. @@ -342,7 +344,7 @@ The last column of the table though is not about the screen: it indicates if the - | stderr | permanent | timestamp - - | first line only, + - | first line only, | without progress * - ``.open_stream(...)`` - -- @@ -391,7 +393,7 @@ The last column of the table though is not about the screen: it indicates if the - | stderr | permanent | timestamp - - | only when + - | only when | level=trace * - | **captured logs** | (level > ``logging.DEBUG``) @@ -428,7 +430,7 @@ The last column of the table though is not about the screen: it indicates if the - | stderr | permanent | timestamp - - | only when + - | only when | level=trace (*) when redirected to a file it doesn't make sense to have "transient" messages, so 'progress' messages will always end in a newline, and 'progress_bar' will just send its message line but without the progress indication. diff --git a/docs/howtos.rst b/docs/howtos.rst index cffcb45..079f10f 100644 --- a/docs/howtos.rst +++ b/docs/howtos.rst @@ -1,3 +1,5 @@ +.. _howtos: + ******* HOW TOs ******* @@ -7,7 +9,7 @@ HOW TOs Use a different logfile structure than the default ================================================== -To override :ref:`the default management of application log files `, a file path can be specified when initiating the `emit` object, using the ``log_filepath`` parameter:: +To override :ref:`the default management of application log files `, a file path can be specified when initiating the ``emit`` object, using the ``log_filepath`` parameter:: emit.init(mode, appname, greeting, log_filepath) @@ -55,11 +57,11 @@ In the following code structure we see all these effects at once:: In detail: -- the return code from the command's execution is bound when calling `dispatcher.run`, supporting the case of it not returning anything (defaults to `0`) +- the return code from the command's execution is bound when calling ``dispatcher.run``, supporting the case of it not returning anything (defaults to ``0``) -- have different return codes assigned for the different `except` situations, with two particular cases: for ``ProvideHelpException`` it's `0` as it's a normal exit situation when the user requested for help, and for ``CraftError`` where the return code is taken from the exception itself +- have different return codes assigned for the different ``except`` situations, with two particular cases: for ``ProvideHelpException`` it's ``0`` as it's a normal exit situation when the user requested for help, and for ``CraftError`` where the return code is taken from the exception itself -- a `sys.exit` at the very end for the process to return the value +- a ``sys.exit`` at the very end for the process to return the value Raise more informational errors @@ -78,13 +80,13 @@ So, in addition of just passing a message to the user... - ``details``: full error details received from a third party or extended information about the situation, useful for debugging but not to be normally shown to the user. E.g.:: raise CraftError( - "Cannot access the indicated file.", + "Cannot access the indicated file.", details=f"File permissions: {oct(filepath.stat().st_mode)}") raise CraftError( f"Server returned bad code {error_code}", details=f"Full server response: {response.content!r}") - + - ``resolution``: an extra line indicating to the user how the error may be fixed or avoided. E.g.:: @@ -128,7 +130,7 @@ To define more automatic global arguments than the ones provided automatically b Check :class:`craft_cli.dispatcher.GlobalArgument` for more information about the parameters needed, but it's very straightforward to create these objects. E.g.:: ga_sec = GlobalArgument("secure_mode", "flag", "-s", "--secure", "Run the app in secure mode") - + To use it, just pass a list of the needed global arguments to the dispatcher using the ``extra_global_args`` option:: dispatcher = Dispatcher(..., extra_global_args=[ga_sec]) @@ -189,7 +191,7 @@ The following example shows a simple usage, please refer to :class:`craft_cli.py Have a hidden option in a command ================================= -To have a command with an option that should not be shown in the help messages, effectively hidden from final users (e.g. because it's experimental), just use a special value in the option's `help`:: +To have a command with an option that should not be shown in the help messages, effectively hidden from final users (e.g. because it's experimental), just use a special value in the option's ``help``:: def fill_parser(self, parser): ... diff --git a/docs/index.rst b/docs/index.rst index 47ebb19..8cfbeff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,21 +1,35 @@ Welcome to Craft CLI's documentation! -=========================================== +===================================== .. toctree:: - :caption: Getting started :maxdepth: 2 + :hidden: tutorials - howtos - + reference explanations -.. toctree:: - :caption: Reference: - :maxdepth: 3 +.. grid:: 1 1 2 2 + + .. grid-item-card:: :ref:`Tutorial ` + + **Get started** with hands-on introductions to Craft CLI + + .. grid-item-card:: :ref:`How-to guides ` + + **Step-by-step guides** covering key operations and common tasks + +.. grid:: 1 1 2 2 + :reverse: + + .. grid-item-card:: :ref:`Reference ` + + **Technical information** about Craft CLI + + .. grid-item-card:: :ref:`Explanation ` - craft_cli + **Discussion and clarification** of key topics Indices and tables diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000..fc19e47 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,10 @@ +.. _reference: + +********* +Reference +********* + +.. toctree:: + :maxdepth: 2 + + craft_cli diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index e1ce92c..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Sphinx==4.2.0 -sphinx-autodoc-typehints==1.12.0 -sphinx-jsonschema==1.16.11 -sphinx-pydantic==0.1.1 -sphinx-rtd-theme==1.0.0 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==2.0.0 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 diff --git a/docs/tutorials.rst b/docs/tutorials.rst index d086a56..04a3d9b 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -1,3 +1,5 @@ +.. _tutorials: + ********* Tutorials ********* @@ -7,7 +9,7 @@ Run a command based application with craft-cli This tutorial will explain how to use Craft CLI to run an application that is based on commands. -Along the way you will define a simple command (named `unlink`, with the functionality of removing files), and call the appropriate library mechanisms for that command to be executed when running the application. +Along the way you will define a simple command (named ``unlink``, with the functionality of removing files), and call the appropriate library mechanisms for that command to be executed when running the application. Prerequisites @@ -32,34 +34,34 @@ Then enable the virtual environment and install Craft CLI:: Define the command and run it using the Dispatcher -------------------------------------------------- -First start with a class sub-classing `BaseCommand` with the appropriate attributes to name it and have automatic help texts, then provide a `fill_parser` method to declare what arguments are possible for this command, and finally a `run` method where the "real" functionality is implemented:: +First start with a class sub-classing ``BaseCommand`` with the appropriate attributes to name it and have automatic help texts, then provide a ``fill_parser`` method to declare what arguments are possible for this command, and finally a ``run`` method where the "real" functionality is implemented:: import pathlib import textwrap import sys from craft_cli import ( - ArgumentParsingError, - BaseCommand, + ArgumentParsingError, + BaseCommand, CommandGroup, - CraftError, - Dispatcher, + CraftError, + Dispatcher, EmitterMode, ProvideHelpException, - emit, + emit, ) class RemoveFileCommand(BaseCommand): """Remove the indicated file.""" - + name = "unlink" help_msg = "Remove the indicated file." overview = textwrap.dedent(""" Remove the indicated file. - + A file needs to be indicated. It is an argument error if the path does not exist or it's a directory. - + It will return successfully if the file was properly removed. """) @@ -99,32 +101,32 @@ Then initiate the ``emit`` object and call the ``Dispatcher`` functionality:: error.__cause__ = exc emit.error(error) except Exception as exc: - error = CraftError(f"Application internal error: {exc!r}") + error = CraftError(f"Application internal error: {exc!r}") error.__cause__ = exc emit.error(error) else: emit.ended_ok() -Finally, put both chunks of code in a ``example-app.py`` file, and (having the virtual environment you prepared at the beginning still activated), run it. You should see the help message for the whole application (as a command is missing, which would be the same output if you pass the `help`, `-h` or `--help` parameters):: +Finally, put both chunks of code in a ``example-app.py`` file, and (having the virtual environment you prepared at the beginning still activated), run it. You should see the help message for the whole application (as a command is missing, which would be the same output if you pass the ``help``, ``-h`` or ``--help`` parameters):: $ python example-app.py Usage: example-app [help] - + Summary: Example application for the craft-cli tutorial. - + Global options: -h, --help: Show this help message and exit -v, --verbose: Show debug information and be more verbose -q, --quiet: Only show warnings and errors, not progress - --verbosity: Set the verbosity level to 'quiet', 'brief', + --verbosity: Set the verbosity level to 'quiet', 'brief', 'verbose', 'debug' or 'trace'", - + Starter commands: - + Commands can be classified as follows: Example: unlink - + For more information about a command, run 'example-app help '. For a summary of all commands, run 'example-app help --all'. @@ -133,30 +135,30 @@ Ask help for specifically for the command:: $ python example-app.py help unlink Usage: example-app unlink [options] - + Summary: Remove the indicated file. - + A file needs to be indicated. It is an argument error if the path does not exist or it's a directory. - + It will return successfully if the file was properly removed. - + Options: -h, --help: Show this help message and exit -v, --verbose: Show debug information and be more verbose -q, --quiet: Only show warnings and errors, not progress - --verbosity: Set the verbosity level to 'quiet', 'brief', + --verbosity: Set the verbosity level to 'quiet', 'brief', 'verbose', 'debug' or 'trace'", - + For a summary of all commands, run 'example-app help --all'. - + Time to run the command on a file, you should see the successful message:: $ touch testfile $ ls testfile testfile - $ env/bin/python example-app.py unlink testfile + $ env/bin/python example-app.py unlink testfile File removed successfully. $ ls testfile ls: cannot access 'testfile': No such file or directory @@ -166,9 +168,9 @@ Explore different error situations, first trying to remove a directory, then try $ mkdir testdir $ python example-app.py unlink testdir The indicated path is not a file or does not exist. - + $ touch /tmp/testfile - $ sudo chown root /tmp/testfile + $ sudo chown root /tmp/testfile $ python example-app.py unlink /tmp/testfile Problem removing the file: [Errno 1] Operation not permitted: '/tmp/testfile'. Full execution log: '/home/user/.cache/example-app/log/example-app-20220114-120745.861866.log' diff --git a/pyproject.toml b/pyproject.toml index 135533b..d0813d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,264 @@ +[project] +name = "craft-cli" +dynamic = ["version"] +description = "Command Line Interface" +authors = [ + {name = "Canonical Ltd", email = "snapcraft@lists.snapcraft.io"} +] +dependencies = [ + "platformdirs", + "pydantic", + "pyyaml", + "pywin32; sys_platform == 'win32'" +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +requires-python = ">=3.8" + +[project.license] +file = "LICENSE" + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.urls] +Website = "https://github.com/canonical/craft-cli" +Documentation = "https://craft-cli.readthedocs.io/en/latest/" +Source = "https://github.com/canonical/craft-cli.git" +Issues = "https://github.com/canonical/craft-cli/issues" + +[project.entry-points.pytest11] +emitter = "craft_cli.pytest_plugin" + +[project.optional-dependencies] +dev = [ + "coverage[toml]==7.2.7", + "pytest==7.3.1", + "pytest-cov==4.1.0", + "pytest-mock==3.10.0", + "pytest-subprocess" +] +lint = [ + "black==23.3.0", + "codespell[toml]==2.2.4", + "pylint==2.17.4", + "pylint-fixme-info==1.0.3", + "pylint-pytest==1.1.2", + "ruff==0.0.269", + "yamllint==1.32.0" +] +types = [ + "mypy[reports]==1.3.0", + "pyright==1.1.311", +] +docs = [ + "furo==2023.5.20", + "sphinx>=6.2.1,<7.0", + "sphinx-autobuild==2021.3.14", + "sphinx-autodoc-typehints", + "sphinx-copybutton==0.5.2", + "sphinx-design==0.4.1", + "sphinx-pydantic==0.1.1", + "sphinx-toolbox==3.4.0", + "sphinx-lint==0.6.7", +] + +[build-system] +requires = [ + "setuptools==67.7.2", + "setuptools_scm[toml]>=7.1" +] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +readme = {file = "README.rst"} + +[tool.setuptools_scm] +write_to = "craft_cli/_version.py" + +[tool.setuptools.packages.find] +exclude = [ + "dist*", + "docs*", + "results*", + "tests*", +] + +[tool.black] +target-version = ["py38"] +line-length = 99 + +[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 force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true -line_length = 99 +line_length = 88 -[tool.black] -line-length = 99 +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = "tests" +xfail_strict = true + +[tool.coverage.run] +branch = true +parallel = true +omit = ["tests/**"] + +[tool.coverage.report] +skip_empty = true +fail_under = 74 [tool.pyright] +#strict = ["craft_cli"] # NOTE: disabled pending fix of the many errors (see gh issue #147) pythonVersion = "3.8" +pythonPlatform = "Linux" +ignore = [ + "docs/*" +] + +[tool.mypy] +python_version = "3.8" exclude = [ - "**/__pycache__", - "**/.mypy_cache", - "**/.pytest_cache", - ".direnv", "build", + "tests", + "results", "docs", - "venv" ] +warn_unused_configs = true +warn_redundant_casts = true +strict_equality = true +strict_concatenate = true +warn_return_any = true +disallow_subclassing_any = true +disallow_untyped_decorators = true +disallow_any_generics = true + +[[tool.mypy.overrides]] +module = ["craft_cli"] +disallow_untyped_defs = true +no_implicit_optional = true + +[[tool.mypy.overrides]] +module = ["tests.*"] +strict = false + +[tool.ruff] +line-length = 99 +target-version = "py38" +src = ["craft_cli", "tests"] +extend-exclude = [ + "docs", + "__pycache__", + # NOTE: disabling ruff to handle in a separate issue (#146) + "craft_cli", + "examples.py", + "tests", +] +# Follow ST063 - Maintaining and updating linting specifications for updating these. +select = [ # Base linting rule selections. + # See the internal document for discussion: + # https://docs.google.com/document/d/1i1n8pDmFmWi4wTDpk-JfnWCVUThPJiggyPi2DYwBBu4/edit + # All sections here are stable in ruff and shouldn't randomly introduce + # failures with ruff updates. + "F", # The rules built into Flake8 + "E", "W", # pycodestyle errors and warnings + "I", # isort checking + "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. + "BLE", # Do not catch blind exceptions + "FBT", # Disallow boolean positional arguments (make them keyword-only) + "B0", # Common mistakes and typos. + "A", # Shadowing built-ins. + "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 + "PYI", # Linting for type stubs. + "PT", # Pytest + "Q", # Consistent quotations + "RSE", # Errors on pytest raises. + "RET", # Simpler logic after return, raise, continue or break + "SIM", # Code simplification + "TCH", # Guard imports only used for type checking behind a type-checkning block. + "ARG", # Unused arguments + "PTH", # Migrate to pathlib + "ERA", # Don't check in commented out code + "PGH", # Pygrep hooks + "PL", # Pylint + "TRY", # Cleaner try/except, +] +extend-select = [ + # 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 + "B026", # Keyword arguments must come after starred arguments + # 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 + "S110", # try-except-pass (use contextlib.suppress instead) + "S113", # Requests calls without timeouts + "S3", # Serialising, deserialising, hashing, crypto, etc. + "S506", # Unsafe YAML load + "S508", "S509", # Insecure SNMP + "S701", # jinja2 templates without autoescape + "RUF001", "RUF002", "RUF003", # Ambiguous unicode characters + "RUF005", # Encourages unpacking rather than concatenation + "RUF008", # Do not use mutable default values for dataclass attributes + "RUF100", # #noqa directive that doesn't flag anything +] +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) + "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) + "A003", # Class attribute shadowing built-in (reason: Class attributes don't often get bare references) + + # Ignored due to common usage in current code + "TRY003", # Avoid specifying long messages outside the exception class +] + +[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 + "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 +] +# isort leaves init files alone by default, this makes ruff ignore them too. +"__init__.py" = ["I001"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8777be4..0000000 --- a/setup.cfg +++ /dev/null @@ -1,120 +0,0 @@ -[metadata] -name = craft-cli -version = attr: craft_cli.__version__ -description = "Command Line Interface" -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/canonical/craft-cli -project_urls = - Documentation = https://craft-cli.readthedocs.io/en/latest/ - Source = https://github.com/canonical/craft-cli.git - Issues = https://github.com/canonical/craft-cli/issues -author = Canonical Ltd. -author_email = snapcraft@lists.snapcraft.io -license = GNU Lesser General Public License v3 (LGPLv3) -license_files = LICENSE -classifiers = - Development Status :: 5 - Production/Stable - Framework :: Pytest - Intended Audience :: Developers - License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) - Operating System :: MacOS :: MacOS X - Operating System :: Microsoft :: Windows - Operating System :: POSIX :: Linux - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >= 3.8 -include_package_data = True -packages = find: -zip_safe = False -install_requires = - platformdirs - pydantic - pyyaml - pywin32; sys_platform == "win32" - -[options.package_data] -craft_cli = py.typed - -[options.extras_require] -doc = - sphinx - sphinx-autodoc-typehints - sphinx-pydantic - sphinx-rtd-theme -release = - twine - wheel -test = - black - codespell - coverage - flake8 - isort - mypy - pydocstyle - pylint - pylint-fixme-info - pylint-pytest - pytest - pytest-mock - pytest-subprocess - tox - types-pyyaml - types-requests - types-setuptools -dev = - autoflake - %(doc)s - %(release)s - %(test)s - -[options.packages.find] -exclude = - tests - tests.* - -[options.entry_points] -pytest11 = - emitter = craft_cli.pytest_plugin - -[bdist_wheel] -universal = 1 - -[codespell] -quiet-level = 3 -skip = ./docs/_build,.direnv,.git,.mypy_cache,.pytest_cache,.venv,__pycache__,venv -ignore-words-list = dedented - -[flake8] -exclude = .direnv .git .mypy_cache .pytest_cache .venv __pycache__ venv -max-line-length = 99 -# E501 line too long -extend-ignore = E501 - -[mypy] -python_version = 3.8 -plugins = pydantic.mypy - -[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 deleted file mode 100644 index ee9f9d1..0000000 --- a/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2021 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# - -"""The setup script.""" - -from setuptools import setup # type: ignore - -setup() diff --git a/tox.ini b/tox.ini index 573d60f..4630f4e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,130 @@ [tox] -envlist = py38 +env_list = # Environments to run when called with no parameters. + lint-{black,ruff,pyright,shellcheck,codespell,docs,pylint} + test-{py38,py310,py311} +minversion = 4.5 +# 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 +# versions of tox. Tox >= 3.8 will automatically provision the version provided +# inside of a virtual environment, so users of Ubuntu >= focal can simply +# 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 +# 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. +user_tmp_dir = {env:TMPDIR} -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = - .[dev] +[testenv] # Default config for all environments. Overridable in each env. +# We have many tests that create temporary files. Unless the user has set a +# TMPDIR, this will prefer putting those temp files in $XDG_RUNTIME_DIR, +# which will speed up those tests since they'll run on a ramdisk. +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} + +[test] # Base configuration for unit and integration tests +package = editable +extras = dev +allowlist_externals = mkdir +commands_pre = mkdir -p results + +[testenv:test-{py38,py39,py310,py311,py312}] # Configuration for all tests using pytest +base = testenv, test +description = Run unit tests with pytest +labels = + py38, py310, py311: tests, unit-tests +commands = pytest {tty:--color=yes} --cov --cov-report=xml:results/coverage-{env_name}.xml --junit-xml=results/test-results-{env_name}.xml tests/unit {posargs} + +[testenv:integration-{py38,py39,py310,py311,py312}] +base = testenv, test +description = Run integration tests with pytest +labels = + py38, py310, py311: tests, integration-tests +commands = pytest {tty:--color=yes} --junit-xml=results/test-results-{env_name}.xml tests/integration {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,pylint}] +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 = - pip install -U pip - pytest --basetemp={envtmpdir} + 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} . + pylint: pylint craft_cli {posargs} --ignore _version.py + pylint: pylint tests {posargs} --disable=missing-module-docstring,missing-function-docstring,redefined-outer-name,protected-access,duplicate-code,too-many-lines,missing-class-docstring,too-few-public-methods -[testenv:docs] -commands = make docs +[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 .mypy_cache +commands = + pyright: pyright {posargs} + mypy: mypy --install-types --non-interactive {posargs:.} -[testenv:lint] -commands = make lint +[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:integrations] -commands = make test-integrations +[docs] # Sphinx documentation configuration +extras = docs +package = editable +no_package = true +env_dir = {work_dir}/docs +runner = ignore_env_name_mismatch + +[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' + bash -c 'rm -f {tox_root}/docs/craft_cli*.rst' +commands = + sphinx-build {posargs:-b html} -v {tox_root}/docs {tox_root}/docs/_build -[testenv:units] -commands = make test-units +[testenv:autobuild-docs] +description = Build documentation with an autoupdating server +base = docs +allowlist_externals = + bash +commands_pre = + bash -c 'rm -f {tox_root}/docs/craft_cli*.rst' +commands = sphinx-autobuild {posargs:-b html --open-browser --port 8080} --watch {tox_root}/craft_cli {tox_root}/docs {tox_root}/docs/_build -[pycodestyle] -ignore = E501 +[testenv:lint-docs] +description = Lint the documentation with sphinx-lint +base = docs +commands = sphinx-lint --ignore docs/_build -e all --disable line-too-long {posargs} docs/ +labels = lint