diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 4766b98..405720d 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -22,29 +22,17 @@ jobs: needs: release-please if: ${{ needs.release-please.outputs.release_created }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v2 - with: - python-version: "3.11" - - - name: Install poetry - run: pip install poetry - - - name: Determine dependencies - run: poetry lock + - name: Setup pixi + uses: prefix-dev/setup-pixi@v0 - - uses: actions/setup-python@v4 - with: - python-version: "3.11" - cache: poetry - - - name: Install Dependencies using Poetry + - name: Build source and wheel distribution + check build run: | - poetry install + pixi run check-build - - name: Publish to PyPi - env: - PYPI_USERNAME: __token__ - PYPI_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: poetry publish --build --username $PYPI_USERNAME --password $PYPI_PASSWORD + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b5fe0c7..5be4ed4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,62 +8,40 @@ on: branches-ignore: [] jobs: - linting: + formatting: runs-on: ubuntu-latest steps: - name: Check out the code - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - - name: Install poetry - run: pip install poetry - - - name: Determine dependencies - run: poetry lock + uses: actions/checkout@v5 - - uses: actions/setup-python@v4 - with: - python-version: "3.11" - cache: poetry - - - name: Install Dependencies using Poetry - run: poetry install + - name: Setup pixi + uses: prefix-dev/setup-pixi@v0 - name: Check formatting - run: poetry run ruff format --check . - - - name: Lint - run: poetry run ruff check . + run: pixi run format --check . - testing: + linting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 - with: - python-version: "3.11" + - name: Check out the code + uses: actions/checkout@v5 - - name: Install poetry - run: pip install poetry + - name: Setup pixi + uses: prefix-dev/setup-pixi@v0 - - name: Determine dependencies - run: poetry lock + - name: Check code + run: pixi run lint - - uses: actions/setup-python@v4 - with: - python-version: "3.11" - cache: poetry + testing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 - - name: Install dependencies - run: | - poetry install + - name: Setup pixi + uses: prefix-dev/setup-pixi@v0 - name: Run pytest - run: poetry run coverage run -m pytest tests/tests.py + run: pixi run test -v - name: Run Coverage - run: poetry run coverage report -m + run: pixi run coverage-report diff --git a/.gitignore b/.gitignore index bcc3d59..6414d09 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ *.py[cod] *$py.class +pixi.lock + # C extensions *.so @@ -159,4 +161,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -poetry.lock \ No newline at end of file +poetry.lock diff --git a/pyproject.toml b/pyproject.toml index 095b9e4..13e321e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,72 @@ -[tool.poetry] -authors = ["Johannes Köster "] +[project] +authors = [ + { name = "Johannes Köster", email = "johannes.koester@uni-due.de" } +] description = "This package provides a stable interface for interactions between Snakemake and its software deployment plugins." license = "MIT" name = "snakemake-interface-software-deployment-plugins" -packages = [{include = "snakemake_interface_software_deployment_plugins"}] readme = "README.md" version = "0.9.0" +requires-python = ">=3.11,<4.0" +dependencies = [ + "argparse-dataclass >=2.0.0,<3.0", + "snakemake-interface-common >=1.17.4,<2.0.0" +] -[tool.poetry.dependencies] -argparse-dataclass = "^2.0.0" -python = "^3.11" -snakemake-interface-common = "^1.17.4" +[project.urls] +repository = "https://github.com/snakemake/snakemake-interface-software-deployment-plugins" -[tool.poetry.group.dev.dependencies] -coverage = {extras = ["toml"], version = "^6.3.1"} -flake8-bugbear = "^22.1.11" -pytest = "^7.0" -ruff = "^0.9.9" -snakemake-software-deployment-plugin-envmodules = "^0.1.2" +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pixi.pypi-dependencies] +snakemake-interface-software-deployment-plugins = { path = ".", editable = true } + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["osx-arm64", "linux-64"] + +[tool.pixi.environments] +dev = { features = ["dev"] } +publish = { features = ["publish"] } + +[tool.pixi.feature.dev.dependencies] +pytest = ">=8.3.5,<9" +ruff = ">=0.10.0,<0.11" +pytest-cov = ">=6.0.0,<7" +pyrefly = ">=0.52.0,<0.53" -[tool.coverage.run] -omit = [".*", "*/site-packages/*"] +[tool.pixi.feature.dev.pypi-dependencies] +snakemake-software-deployment-plugin-envmodules = ">=0.1.2,<1.0" + +[tool.pixi.feature.dev.tasks] +format = "ruff format" +lint = "ruff check" +typecheck = "pyrefly check" +qc = { depends-on = ["format", "lint"] } +coverage-report = "coverage report -m" + +[tool.pixi.feature.dev.tasks.test] +cmd = [ + "pytest", + "--cov=snakemake_software_deployment_plugin_envmodules", + "--cov-report=xml:coverage-report/coverage.xml", + "--cov-report=term-missing", + "tests/test_interface.py", +] [tool.coverage.report] -fail_under = 60 +exclude_lines = ["pass", "\\.\\.\\."] +fail_under = 63.0 -[build-system] -build-backend = "poetry.core.masonry.api" -requires = ["poetry-core"] +[tool.pixi.feature.publish.dependencies] +twine = ">=6.1.0,<7" +python-build = ">=1.2.2,<2" + + +[tool.pixi.feature.publish.tasks] +build = { cmd = "python -m build", description = "Build the package into the dist/ directory" } +check-build = { cmd = "python -m twine check dist/*", depends-on = [ + "build", +], description = "Check that the package can be uploaded" } diff --git a/snakemake_interface_software_deployment_plugins/__init__.py b/snakemake_interface_software_deployment_plugins/__init__.py index ec583e8..8f5d0a0 100644 --- a/snakemake_interface_software_deployment_plugins/__init__.py +++ b/snakemake_interface_software_deployment_plugins/__init__.py @@ -5,10 +5,12 @@ from abc import ABC, abstractmethod from copy import copy +from inspect import getmodule from dataclasses import dataclass, field import hashlib from pathlib import Path import shutil +from types import ModuleType from typing import ( Any, ClassVar, @@ -19,6 +21,7 @@ Self, Tuple, Type, + TypeVar, Union, ) import subprocess as sp @@ -43,16 +46,22 @@ class EnvSpecSourceFile: class EnvSpecBase(ABC): + @classmethod + def module(cls) -> ModuleType: + class_module = getmodule(cls) + assert class_module is not None, f"bug: cannot detect class module of {cls}" + return class_module + def technical_init(self): """This has to be called by Snakemake upon initialization""" self.within: Optional["EnvSpecBase"] = None self.fallback: Optional["EnvSpecBase"] = None - self.kind: str = self.__class__.__module__.common_settings.provides + self.kind: str = self.module().common_settings.provides self._obj_hash: Optional[int] = None @classmethod def env_cls(cls): - return cls.__module__.EnvBase + return cls.module().EnvBase @classmethod @abstractmethod @@ -148,19 +157,11 @@ def run(self, cmd: str, **kwargs) -> sp.CompletedProcess: return sp.run([self.executable] + self.args + [self.command_arg, cmd], **kwargs) +TSettings = TypeVar("TSettings", bound="SoftwareDeploymentSettingsBase") + + class EnvBase(ABC): _cache: ClassVar[Dict[Tuple[Type["EnvBase"], Optional["EnvBase"]], Any]] = {} - spec: EnvSpecBase - within: Optional["EnvBase"] - settings: Optional[SoftwareDeploymentSettingsBase] - shell_executable: ShellExecutable - tempdir: Path - _cache_prefix: Path - _deployment_prefix: Path - _pinfile_prefix: Path - _managed_hash_store: Optional[str] = None - _managed_deployment_hash_store: Optional[str] = None - _obj_hash: Optional[int] = None def __init__( self, @@ -174,8 +175,8 @@ def __init__( deployment_prefix: Path, pinfile_prefix: Path, ): - self.spec: EnvSpecBase = spec - self.within: Optional["EnvBase"] = within + self.spec = spec + self.within = within self.settings: Optional[SoftwareDeploymentSettingsBase] = settings self.shell_executable = shell_executable self.tempdir = tempdir @@ -183,6 +184,9 @@ def __init__( self._deployment_prefix: Path = deployment_prefix self._cache_prefix: Path = cache_prefix self._pinfile_prefix: Path = pinfile_prefix + self._managed_hash_store: Optional[str] = None + self._managed_deployment_hash_store: Optional[str] = None + self._obj_hash: Optional[int] = None self.__post_init__() def __post_init__(self) -> None: # noqa B027 diff --git a/tests/tests.py b/tests/test_interface.py similarity index 86% rename from tests/tests.py rename to tests/test_interface.py index f9020fb..ffda205 100644 --- a/tests/tests.py +++ b/tests/test_interface.py @@ -20,8 +20,8 @@ def get_test_plugin_name(self) -> str: def validate_plugin(self, plugin: PluginBase): assert plugin.settings_cls is None - assert plugin.env_cls is not None - assert plugin.env_spec_cls is not None + assert plugin.env_cls is not None # pyrefly: ignore[missing-attribute] + assert plugin.env_spec_cls is not None # pyrefly: ignore[missing-attribute] def validate_settings(self, settings: SettingsBase, plugin: PluginBase): # assert isinstance(settings, plugin.settings_cls)