diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 53941ac..d6e9b4f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,7 +24,7 @@ jobs: - "3.8" - "3.7" os: - - ubuntu-22.04 + - ubuntu-latest - windows-2022 - macos-12 @@ -65,7 +65,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-22.04 + - ubuntu-latest - windows-2022 tox_env: - dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 322ff5f..3515a30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: jobs: release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest environment: name: release url: https://pypi.org/p/devpi-process diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9de12dc..763b550 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,68 +2,32 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: check-ast - - id: check-builtin-literals - - id: check-docstring-first - - id: check-merge-conflict - - id: check-yaml - - id: check-toml - - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.0.272" hooks: - - id: pyupgrade - args: ["--py37-plus"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - args: [--safe] - - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==23.3] - - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.11.1" - hooks: - - id: pyproject-fmt - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.3.0" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.11.2" hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.3.23 - - flake8-comprehensions==3.12 - - flake8-pytest-style==1.7.2 - - flake8-spellcheck==0.28 - - flake8-unused-arguments==0.0.13 - - flake8-noqa==1.3.1 - - pep8-naming==0.13.3 - - flake8-pyproject==1.2.3 + - id: pyproject-fmt + additional_dependencies: ["tox>=4.6"] - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v2.7.1" + rev: "v3.0.0-alpha.9-for-vscode" hooks: - id: prettier - additional_dependencies: - - prettier@2.7.1 - - "@prettier/plugin-xml@2.2" args: ["--print-width=120", "--prose-wrap=always"] - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.34.0 - hooks: - - id: markdownlint - repo: meta hooks: - id: check-hooks-apply diff --git a/pyproject.toml b/pyproject.toml index afcac5a..1427151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,19 +62,6 @@ version.source = "vcs" [tool.black] line-length = 120 -[tool.isort] -profile = "black" -known_first_party = ["devpi_process"] -line_length = 120 -add_imports = ["from __future__ import annotations"] - -[tool.flake8] -max-complexity = 22 -max-line-length = 120 -unused-arguments-ignore-abstract-functions = true -noqa-require-code = true -dictionaries = ["en_US", "python", "technical", "django"] - [tool.coverage] html.show_contexts = true html.skip_covered = false @@ -96,5 +83,24 @@ python_version = "3.11" show_error_codes = true strict = true -[tool.pep8] -max-line-length = "120" +[tool.ruff] +select = ["ALL"] +line-length = 120 +target-version = "py37" +isort = {known-first-party = ["devpi_process"], required-imports = ["from __future__ import annotations"]} +ignore = [ + "ANN101", # no typoe annotation for self + "ANN401", # allow Any as type annotation + "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible + "S104", # Possible binding to all interface +] +[tool.ruff.per-file-ignores] +"tests/**/*.py" = [ + "S101", # asserts allowed in tests... + "FBT", # don"t care about booleans as positional arguments in tests + "INP001", # no implicit namespace + "D", # don"t care about documentation in tests + "S603", # `subprocess` call: check for execution of untrusted input + "PLR2004", # Magic value used in comparison, consider replacing with a constant variable +] diff --git a/src/devpi_process/__init__.py b/src/devpi_process/__init__.py index 1fb3b85..65b3bda 100644 --- a/src/devpi_process/__init__.py +++ b/src/devpi_process/__init__.py @@ -1,3 +1,4 @@ +"""Devpi PyPI to test with.""" from __future__ import annotations import random @@ -9,18 +10,30 @@ from pathlib import Path from subprocess import PIPE, Popen, run from threading import Thread -from types import TracebackType -from typing import IO, Iterator, Sequence, cast +from typing import IO, TYPE_CHECKING, Iterator, Sequence, cast from ._version import __version__ +if TYPE_CHECKING: + from types import TracebackType + def _check_call(cmd: list[str]) -> None: - run(cmd, check=True, capture_output=True) + run(cmd, check=True, capture_output=True) # noqa: S603 class Index: + """Index.""" + def __init__(self, base_url: str, name: str, user: str, client_cmd_base: list[str]) -> None: + """ + Create index. + + :param base_url: base url + :param name: name for the index server + :param user: the username to use + :param client_cmd_base: + """ self._client_cmd_base = client_cmd_base self._server_url = base_url self.name = name @@ -28,31 +41,54 @@ def __init__(self, base_url: str, name: str, user: str, client_cmd_base: list[st @property def url(self) -> str: + """:return: the URL to the index server""" return f"{self._server_url}/{self.name}/+simple/" def use(self) -> None: - _check_call(self._client_cmd_base + ["use", f"{self.user}/{self.name}"]) + """Use this index server.""" + _check_call([*self._client_cmd_base, "use", f"{self.user}/{self.name}"]) def upload(self, *files: Path) -> None: + """ + Upload packages to the index. + + :param files: the files to upload + """ cmd = self._client_cmd_base + ["upload", "--index", self.name] + [str(i) for i in files] _check_call(cmd) def __repr__(self) -> str: + """:return: repr of the index""" return f"{self.__class__.__name__}(url={self.url})" class IndexServer: - def __init__(self, path: Path, with_root_pypi: bool = False, start_args: Sequence[str] | None = None) -> None: + """A PyPI index server locally.""" + + def __init__( + self, + path: Path, + with_root_pypi: bool = False, # noqa: FBT001, FBT002 + start_args: Sequence[str] | None = None, + ) -> None: + """ + Create the local index server. + + :param path: the path where to host files + :param with_root_pypi: access to upstream PyPI + :param start_args: additional arguments to start the server + """ self.path = path self._with_root_pypi = with_root_pypi self._start_args: Sequence[str] = [] if start_args is None else start_args self.host, self.port = "localhost", _find_free_port() - self._passwd = "".join(random.choices(string.ascii_letters, k=8)) + self._passwd = "".join(random.choices(string.ascii_letters, k=8)) # noqa: S311 scripts_dir = sysconfig.get_path("scripts") if scripts_dir is None: - raise RuntimeError("could not get scripts folder of host interpreter") # pragma: no cover + msg = "could not get scripts folder of host interpreter" # pragma: no cover + raise RuntimeError(msg) # pragma: no cover def _exe(name: str) -> str: return str(Path(scripts_dir) / f"{name}{'.exe' if sys.platform == 'win32' else ''}") @@ -70,9 +106,11 @@ def _exe(name: str) -> str: @property def user(self) -> str: + """:return: username of the index server""" return "root" def __enter__(self) -> IndexServer: + """:return: start the index server""" self._create_and_start_server() self._setup_client() return self @@ -89,7 +127,7 @@ def _create_and_start_server(self) -> None: # 2. start the server cmd = [self._server, "--serverdir", server_at, "--port", str(self.port)] cmd.extend(self._start_args) - self._process = Popen(cmd, stdout=PIPE, universal_newlines=True) + self._process = Popen(cmd, stdout=PIPE, universal_newlines=True) # noqa: S603 stdout = self._drain_stdout() for line in stdout: # pragma: no branch # will always loop at least once if "serving at url" in line: @@ -108,32 +146,47 @@ def _drain_stdout(self) -> Iterator[str]: stdout = cast(IO[str], process.stdout) while True: if process.poll() is not None: # pragma: no cover - print(f"devpi server with pid {process.pid} at {self._server_dir} died") + print(f"devpi server with pid {process.pid} at {self._server_dir} died") # noqa: T201 break yield stdout.readline() def _setup_client(self) -> None: - """create a user on the server and authenticate it""" + """Create a user on the server and authenticate it.""" self._client_dir.mkdir(exist_ok=True) base = ["--clientdir", str(self._client_dir)] - _check_call([self._client, "use"] + base + [self.url]) - _check_call([self._client, "login"] + base + [self.user, "--password", self._passwd]) + _check_call([self._client, "use", *base, self.url]) + _check_call([self._client, "login", *base, self.user, "--password", self._passwd]) def create_index(self, name: str, *args: str) -> Index: + """ + Create an index on the server. + + :param name: with name + :param args: additional arguments + :return: the created index + """ if name in self._indexes: # pragma: no cover - raise ValueError(f"index {name} already exists") + msg = f"index {name} already exists" + raise ValueError(msg) base = [self._client, "--clientdir", str(self._client_dir)] - _check_call(base + ["index", "-c", name, *args]) + _check_call([*base, "index", "-c", name, *args]) index = Index(f"{self.url}/{self.user}", name, self.user, base) self._indexes[name] = index return index def __exit__( self, - exc_type: type[BaseException] | None, # noqa: U100 - exc_val: BaseException | None, # noqa: U100 - exc_tb: TracebackType | None, # noqa: U100 + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: + """ + Stop the index server. + + :param exc_type: + :param exc_val: + :param exc_tb: + """ if self._process is not None: # pragma: no cover # defend against devpi startup fail self._process.terminate() if self._stdout_drain is not None and self._stdout_drain.is_alive(): # pragma: no cover # devpi startup fail @@ -141,9 +194,11 @@ def __exit__( @property def url(self) -> str: + """:return: url to the index server""" return f"http://{self.host}:{self.port}" def __repr__(self) -> str: + """:return: repr of the index server""" return f"{self.__class__.__name__}(url={self.url}, indexes={list(self._indexes)})" diff --git a/tests/demo_pkg_inline/build.py b/tests/demo_pkg_inline/build.py index 51deca6..7893d8e 100644 --- a/tests/demo_pkg_inline/build.py +++ b/tests/demo_pkg_inline/build.py @@ -1,9 +1,9 @@ from __future__ import annotations -import os import sys import tarfile from io import BytesIO +from pathlib import Path from textwrap import dedent from zipfile import ZipFile @@ -31,7 +31,8 @@ UNKNOWN """.format( - pkg_name, version + pkg_name, + version, ), wheel: """ Wheel-Version: 1.0 @@ -39,7 +40,9 @@ Root-Is-Purelib: true Tag: py{}-none-any """.format( - name, version, sys.version_info[0] + name, + version, + sys.version_info[0], ), f"{dist_info}/top_level.txt": name, record: """ @@ -49,39 +52,40 @@ {1}/top_level.txt,, {1}/RECORD,, """.format( - name, dist_info + name, + dist_info, ), } def build_wheel( wheel_directory: str, - metadata_directory: str | None = None, # noqa: U100 - config_settings: None = None, # noqa: U100 + metadata_directory: str | None = None, # noqa: ARG001 + config_settings: None = None, # noqa: ARG001 ) -> str: base_name = f"{name}-{version}-py{sys.version_info[0]}-none-any.whl" - path = os.path.join(wheel_directory, base_name) - with ZipFile(path, "w") as zip_file_handler: + path = Path(wheel_directory) / base_name + with ZipFile(str(path), "w") as zip_file_handler: for arc_name, data in content.items(): # pragma: no branch zip_file_handler.writestr(arc_name, dedent(data).strip()) return base_name def get_requires_for_build_wheel( - config_settings: None = None, # noqa: U100 + config_settings: None = None, # noqa: ARG001 ) -> list[str]: return [] # pragma: no cover # only executed in non-host pythons def build_sdist( sdist_directory: str, - config_settings: None = None, # noqa: U100 + config_settings: None = None, # noqa: ARG001 ) -> str: result = f"{name}-{version}.tar.gz" - with tarfile.open(os.path.join(sdist_directory, result), "w:gz") as tar: - root = os.path.dirname(os.path.abspath(__file__)) - tar.add(os.path.join(root, "build.py"), "build.py") - tar.add(os.path.join(root, "pyproject.toml"), "pyproject.toml") + with tarfile.open(str(Path(sdist_directory) / result), "w:gz") as tar: + root = Path(__file__).parent + tar.add(str(root / "build.py"), "build.py") + tar.add(str(root / "pyproject.toml"), "pyproject.toml") pkg_info = dedent(content[metadata]).strip().encode("utf-8") info = tarfile.TarInfo("PKG-INFO") @@ -92,6 +96,6 @@ def build_sdist( def get_requires_for_build_sdist( - config_settings: None = None, # noqa: U100 + config_settings: None = None, # noqa: ARG001 ) -> list[str]: return [] # pragma: no cover # only executed in non-host pythons diff --git a/tests/test_devpi_process.py b/tests/test_devpi_process.py index d31df92..655adab 100644 --- a/tests/test_devpi_process.py +++ b/tests/test_devpi_process.py @@ -2,13 +2,16 @@ from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path +from typing import TYPE_CHECKING import pytest -from _pytest.tmpdir import TempPathFactory from httpx import get from devpi_process import IndexServer +if TYPE_CHECKING: + from _pytest.tmpdir import TempPathFactory + def test_version() -> None: import devpi_process @@ -66,5 +69,5 @@ def test_create_server_with_pypi(tmp_path: Path) -> None: def test_create_server_start_args(tmp_path: Path) -> None: with IndexServer(tmp_path, start_args=["--offline-mode"]) as server: - assert server._process is not None - assert server._process.args[-1] == "--offline-mode" # type: ignore + assert server._process is not None # noqa: SLF001 + assert server._process.args[-1] == "--offline-mode" # type: ignore[index] # noqa: SLF001 diff --git a/tox.ini b/tox.ini index a6b4287..fe36586 100644 --- a/tox.ini +++ b/tox.ini @@ -44,12 +44,12 @@ commands = [testenv:type] description = run type check on code base deps = - mypy==1.2 + mypy==1.3 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = - mypy --strict --python-version 3.10 src - mypy --strict --python-version 3.10 tests + mypy src + mypy tests [testenv:readme] description = check that the long description is valid diff --git a/whitelist.txt b/whitelist.txt deleted file mode 100644 index 16e0414..0000000 --- a/whitelist.txt +++ /dev/null @@ -1,10 +0,0 @@ -addfile -devpi -exe -getsockname -inet -mktemp -readline -sdist -sysconfig -tmpdir