From ea20f3b0705b59777d95a4e7a145ac9813b169e0 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 19 Apr 2023 09:24:12 +0200 Subject: [PATCH 1/8] Remove travis Signed-off-by: Cristian Le --- .travis.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 324732d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -sudo: false -language: python -python: - - "2.7" - - "3.4" - - "3.5" - - "3.6" - - "pypy" - - "pypy3" -matrix: - # pypy3 (as of 2.4.0) has a wacky arity issue in its source loader. Allow it - # to fail until we can test on, and require, PyPy3.3+. See invoke#358. - allow_failures: - - python: pypy3 - # Disabled per https://github.com/travis-ci/travis-ci/issues/1696 - # fast_finish: true -install: - - pip install -r dev-requirements.txt -script: - # Run tests w/ coverage first, so it uses the local-installed copy. - # (If we do this after the below installation tests, coverage will think - # nothing got covered!) - - inv coverage --report=xml - # TODO: tighten up these install test tasks so they can be one-shotted - - inv travis.test-installation --package=patchwork --sanity="inv sanity" - - inv travis.test-packaging --package=patchwork --sanity="inv sanity" - - inv docs --nitpick - - flake8 -# TODO: after_success -> codecov, once coverage sucks less XD From 4462c01af4206664cb543ea9f48d0c85df06c4e1 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 19 Apr 2023 10:14:30 +0200 Subject: [PATCH 2/8] Add simple gitignore Signed-off-by: Cristian Le --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b7e1ca4..a51ee18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ docs/_build .cache .coverage +/venv +/build +/.idea +*.egg-info From 6399af1c28ad8c8712bab3dfe0e4209591fb7f4f Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 19 Apr 2023 09:25:05 +0200 Subject: [PATCH 3/8] Add basic pre-commit Signed-off-by: Cristian Le --- .pre-commit-config.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..68d1d2a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.21.0 + hooks: + - id: check-github-workflows From 9b235c33229681419fd2efc3ec9e030a0f683df0 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 19 Apr 2023 09:29:19 +0200 Subject: [PATCH 4/8] Add basic GitHub Action Signed-off-by: Cristian Le --- .github/workflows/tests.yaml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..994ee47 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,42 @@ +name: test +run-name: Run tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + pre-commit: + name: Check pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + - uses: pre-commit/action@v3.0.0 + + pytest: + name: Run pytests + runs-on: ${{ matrix.os }} + needs: [ pre-commit ] + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + include: + - os: ubuntu-20.04 + python-version: "3.6" + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install pytest + run: pip install pytest invocations mock pytest-relaxed + - name: Setup patchwork + run: pip install -e . + - name: Test with pytest + run: pytest From e6c5a2c55686ec32b3d88fff5a6f6ed138df476c Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 15 Mar 2023 14:27:37 +0100 Subject: [PATCH 5/8] Update for fabric3.0 Signed-off-by: Cristian Le --- dev-requirements.txt | 15 ----- pyproject.toml | 63 +++++++++++++++++++ setup.cfg | 14 ----- setup.py | 48 -------------- {patchwork => src/patchwork}/__init__.py | 0 {patchwork => src/patchwork}/_version.py | 0 {patchwork => src/patchwork}/environment.py | 2 +- {patchwork => src/patchwork}/files.py | 16 +++-- {patchwork => src/patchwork}/info.py | 2 +- .../patchwork}/packages/__init__.py | 4 +- {patchwork => src/patchwork}/transfers.py | 14 ++--- {patchwork => src/patchwork}/util.py | 15 ++--- tasks.py | 8 +-- 13 files changed, 90 insertions(+), 111 deletions(-) delete mode 100644 dev-requirements.txt create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py rename {patchwork => src/patchwork}/__init__.py (100%) rename {patchwork => src/patchwork}/_version.py (100%) rename {patchwork => src/patchwork}/environment.py (75%) rename {patchwork => src/patchwork}/files.py (92%) rename {patchwork => src/patchwork}/info.py (96%) rename {patchwork => src/patchwork}/packages/__init__.py (88%) rename {patchwork => src/patchwork}/transfers.py (93%) rename {patchwork => src/patchwork}/util.py (94%) diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 767fe73..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Require a newer fabric than our public API does, since it includes the now -# public test helpers. Bleh. -fabric>=2.1.3,<3 -Sphinx>=1.4,<1.7 -releases>=1.6,<2.0 -alabaster==0.7.12 -wheel==0.24 -twine==1.11.0 -invocations>=1.3.0,<2.0 -pytest-relaxed==1.1.4 -coverage==4.4.2 -pytest-cov==2.4.0 -mock==1.0.1 -flake8==3.5.0 --e . diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..43c53e3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "patchwork" +version = "2.0.0" +description = "Deployment/sysadmin operations, powered by Fabric" +authors = [{ name = "Jeff Forcier", email = "jeff@bitprophet.org" }] +maintainers = [{ name = "Jeff Forcier", email = "jeff@bitprophet.org" }] +requires-python = ">=3.7" +readme = "README.rst" +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Topic :: Software Development", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Software Distribution", + "Topic :: System :: Systems Administration", +] + +dependencies = [ + "fabric>=2.2", +] + +[project.optional-dependencies] +docs = [ + "releases", + "alabaster", + "sphinx", +] +test = [ + "invocations", + "pytest", + "mock", + "pytest-relaxed", +] +test-cov = [ + "pytest-cov", +] +dev = [ + "patchwork[test]", + "pre-commit", +] + +[project.urls] +homepage = "https://www.fabfile.org/" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "*" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 56652a9..0000000 --- a/setup.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[wheel] -universal = 1 - -[metadata] -license_file = LICENSE - -[flake8] -exclude = .git,build,dist -ignore = E124,E125,E128,E261,E301,E302,E303,W503 -max-line-length = 79 - -[tool:pytest] -testpaths = tests -python_files = * diff --git a/setup.py b/setup.py deleted file mode 100644 index 0e92cb5..0000000 --- a/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python - -# Support setuptools only, distutils has a divergent and more annoying API and -# few folks will lack setuptools. -from setuptools import setup, find_packages - -# Version info -- read without importing -_locals = {} -with open("patchwork/_version.py") as fp: - exec(fp.read(), None, _locals) -version = _locals["__version__"] - -setup( - name="patchwork", - version=version, - description="Deployment/sysadmin operations, powered by Fabric", - license="BSD", - long_description=open("README.rst").read(), - author="Jeff Forcier", - author_email="jeff@bitprophet.org", - url="https://fabric-patchwork.readthedocs.io", - install_requires=["fabric>=2.0,<3.0"], - packages=find_packages(), - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: BSD License", - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Topic :: Software Development", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: System :: Software Distribution", - "Topic :: System :: Systems Administration", - ], -) diff --git a/patchwork/__init__.py b/src/patchwork/__init__.py similarity index 100% rename from patchwork/__init__.py rename to src/patchwork/__init__.py diff --git a/patchwork/_version.py b/src/patchwork/_version.py similarity index 100% rename from patchwork/_version.py rename to src/patchwork/_version.py diff --git a/patchwork/environment.py b/src/patchwork/environment.py similarity index 75% rename from patchwork/environment.py rename to src/patchwork/environment.py index ce6763a..c40c773 100644 --- a/patchwork/environment.py +++ b/src/patchwork/environment.py @@ -7,4 +7,4 @@ def have_program(c, name): """ Returns whether connected user has program ``name`` in their ``$PATH``. """ - return c.run("which {}".format(name), hide=True, warn=True) + return c.run(f"which {name}", hide=True, warn=True) diff --git a/patchwork/files.py b/src/patchwork/files.py similarity index 92% rename from patchwork/files.py rename to src/patchwork/files.py index 1e68512..b4b758f 100644 --- a/patchwork/files.py +++ b/src/patchwork/files.py @@ -4,8 +4,6 @@ import re -from invoke.vendor import six - from .util import set_runner @@ -25,12 +23,12 @@ def directory(c, runner, path, user=None, group=None, mode=None): :param str mode: ``chmod`` compatible mode string to apply to the directory. """ - runner("mkdir -p {}".format(path)) + runner(f"mkdir -p {path}") if user is not None: group = group or user - runner("chown {}:{} {}".format(user, group, path)) + runner(f"chown {user}:{group} {path}") if mode is not None: - runner("chmod {} {}".format(mode, path)) + runner(f"chmod {mode} {path}") @set_runner @@ -78,8 +76,8 @@ def contains(c, runner, filename, text, exact=False, escape=True): if escape: text = _escape_for_regex(text) if exact: - text = "^{}$".format(text) - egrep_cmd = 'egrep "{}" "{}"'.format(text, filename) + text = f"^{text}$" + egrep_cmd = f'egrep "{text}" "{filename}"' return runner(egrep_cmd, hide=True, warn=True).ok @@ -116,7 +114,7 @@ def append(c, runner, filename, text, partial=False, escape=True): Whether to perform regex-oriented escaping on ``text``. """ # Normalize non-list input to be a list - if isinstance(text, six.string_types): + if isinstance(text, str): text = [text] for line in text: regex = "^" + _escape_for_regex(line) + ("" if partial else "$") @@ -127,7 +125,7 @@ def append(c, runner, filename, text, partial=False, escape=True): ): continue line = line.replace("'", r"'\\''") if escape else line - runner("echo '{}' >> {}".format(line, filename)) + runner(f"echo '{line}' >> {filename}") def _escape_for_regex(text): diff --git a/patchwork/info.py b/src/patchwork/info.py similarity index 96% rename from patchwork/info.py rename to src/patchwork/info.py index d737d94..12a8ed2 100644 --- a/patchwork/info.py +++ b/src/patchwork/info.py @@ -29,7 +29,7 @@ def distro_name(c): } for name, sentinels in sentinel_files.items(): for sentinel in sentinels: - if exists(c, "/etc/{}".format(sentinel)): + if exists(c, f"/etc/{sentinel}"): return name return "other" diff --git a/patchwork/packages/__init__.py b/src/patchwork/packages/__init__.py similarity index 88% rename from patchwork/packages/__init__.py rename to src/patchwork/packages/__init__.py index d9065d1..2a6aae0 100644 --- a/patchwork/packages/__init__.py +++ b/src/patchwork/packages/__init__.py @@ -6,7 +6,7 @@ # apt/deb, rpm/yum/dnf, arch/pacman, etc etc etc. -from patchwork.info import distro_family +from ..info import distro_family def package(c, *packages): @@ -29,4 +29,4 @@ def rubygem(c, gem): """ Install a Ruby gem. """ - return c.sudo("gem install -b --no-rdoc --no-ri {}".format(gem)) + return c.sudo(f"gem install -b --no-rdoc --no-ri {gem}") diff --git a/patchwork/transfers.py b/src/patchwork/transfers.py similarity index 93% rename from patchwork/transfers.py rename to src/patchwork/transfers.py index e7292da..6bd4285 100644 --- a/patchwork/transfers.py +++ b/src/patchwork/transfers.py @@ -2,8 +2,6 @@ File transfer functionality above and beyond basic ``put``/``get``. """ -from invoke.vendor import six - def rsync( c, @@ -79,7 +77,7 @@ def rsync( (rsync's ``--rsh`` flag.) """ # Turn single-string exclude into a one-item list for consistency - if isinstance(exclude, six.string_types): + if isinstance(exclude, str): exclude = [exclude] # Create --exclude options from exclude list exclude_opts = ' --exclude "{}"' * len(exclude) @@ -97,22 +95,22 @@ def rsync( # always-a-list, always-up-to-date-from-all-sources attribute to save us # from having to do this sort of thing. (may want to wait for Paramiko auth # overhaul tho!) - if isinstance(keys, six.string_types): + if isinstance(keys, str): keys = [keys] if keys: key_string = "-i " + " -i ".join(keys) # Get base cxn params user, host, port = c.user, c.host, c.port - port_string = "-p {}".format(port) + port_string = f"-p {port}" # Remote shell (SSH) options rsh_string = "" # Strict host key checking disable_keys = "-o StrictHostKeyChecking=no" if not strict_host_keys and disable_keys not in ssh_opts: - ssh_opts += " {}".format(disable_keys) + ssh_opts += f" {disable_keys}" rsh_parts = [key_string, port_string, ssh_opts] if any(rsh_parts): - rsh_string = "--rsh='ssh {}'".format(" ".join(rsh_parts)) + rsh_string = f"--rsh='ssh {' '.join(rsh_parts)}'" # Set up options part of string options_map = { "delete": "--delete" if delete else "", @@ -120,7 +118,7 @@ def rsync( "rsh": rsh_string, "extra": rsync_opts, } - options = "{delete}{exclude} -pthrvz {extra} {rsh}".format(**options_map) + options = f"{options_map['delete']}{options_map['exclude']} -pthrvz {options_map['extra']} {options_map['rsh']}" # Create and run final command string # TODO: richer host object exposing stuff like .address_is_ipv6 or whatever if host.count(":") > 1: diff --git a/patchwork/util.py b/src/patchwork/util.py similarity index 94% rename from patchwork/util.py rename to src/patchwork/util.py index 705a475..ff19fe3 100644 --- a/patchwork/util.py +++ b/src/patchwork/util.py @@ -1,11 +1,11 @@ """ Helpers and decorators, primarily for internal or advanced use. """ - +import inspect import textwrap from functools import wraps -from inspect import getargspec, formatargspec +from inspect import getfullargspec, signature # TODO: calling all functions as eg directory(c, '/foo/bar/') (with initial c) @@ -126,18 +126,15 @@ def munge_docstring(f, inner): # Terrible, awful hacks to ensure Sphinx autodoc sees the intended # (modified) signature; leverages the fact that autodoc_docstring_signature # is True by default. - args, varargs, keywords, defaults = getargspec(f) + sig = signature(f) + args = [p.name for p in sig.parameters.values() if p.POSITIONAL_ONLY] + defaults = [p.default for p in sig.parameters.values() if p.default is not p.empty] # Nix positional version of runner arg, which is always 2nd - del args[1] - # Add new args to end in desired order args.extend(["sudo", "runner_method", "runner"]) # Add default values (remembering that this tuple matches the _end_ of the # signature...) defaults = tuple(list(defaults or []) + [False, "run", None]) # Get signature first line for Sphinx autodoc_docstring_signature - sigtext = "{}{}".format( - f.__name__, formatargspec(args, varargs, keywords, defaults) - ) docstring = textwrap.dedent(inner.__doc__ or "").strip() # Construct :param: list params = """:param bool sudo: @@ -147,4 +144,4 @@ def munge_docstring(f, inner): :param runner: Callable runner function or method. Should ideally be a bound method on the given context object! """ # noqa - return "{}\n{}\n\n{}".format(sigtext, docstring, params) + return f"{sig}\n{docstring}\n\n{params}" diff --git a/tasks.py b/tasks.py index e26e4a1..db6abe8 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,6 @@ from importlib import import_module -from invocations import docs, travis +from invocations import docs from invocations.checks import blacken from invocations.packaging import release from invocations.pytest import test, coverage @@ -15,12 +15,12 @@ def sanity(c): """ # Doesn't need to literally import everything, but "a handful" will do. for name in ("environment", "files", "transfers"): - mod = "patchwork.{}".format(name) + mod = f"patchwork.{name}" import_module(mod) - print("Imported {} successfully".format(mod)) + print(f"Imported {mod} successfully") -ns = Collection(docs, release, travis, test, coverage, sanity, blacken) +ns = Collection(docs, release, test, coverage, sanity, blacken) ns.configure( { "packaging": { From c3c286f8e9878baefe5dfd50a9a252252a6139fc Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Fri, 24 Mar 2023 19:02:11 +0100 Subject: [PATCH 6/8] Fix munge_docstring Signed-off-by: Cristian Le --- src/patchwork/util.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/patchwork/util.py b/src/patchwork/util.py index ff19fe3..972d5c5 100644 --- a/src/patchwork/util.py +++ b/src/patchwork/util.py @@ -1,11 +1,10 @@ """ Helpers and decorators, primarily for internal or advanced use. """ -import inspect import textwrap from functools import wraps -from inspect import getfullargspec, signature +from inspect import signature, Parameter # TODO: calling all functions as eg directory(c, '/foo/bar/') (with initial c) @@ -127,13 +126,14 @@ def munge_docstring(f, inner): # (modified) signature; leverages the fact that autodoc_docstring_signature # is True by default. sig = signature(f) - args = [p.name for p in sig.parameters.values() if p.POSITIONAL_ONLY] - defaults = [p.default for p in sig.parameters.values() if p.default is not p.empty] + parameters = list(sig.parameters.values()) # Nix positional version of runner arg, which is always 2nd - args.extend(["sudo", "runner_method", "runner"]) - # Add default values (remembering that this tuple matches the _end_ of the - # signature...) - defaults = tuple(list(defaults or []) + [False, "run", None]) + del parameters[1] + # Append new arguments + parameters.append(Parameter("sudo", Parameter.POSITIONAL_OR_KEYWORD, default=False)) + parameters.append(Parameter("runner_method", Parameter.POSITIONAL_OR_KEYWORD, default="run")) + parameters.append(Parameter("runner", Parameter.POSITIONAL_OR_KEYWORD, default=None)) + sig = sig.replace(parameters=parameters) # Get signature first line for Sphinx autodoc_docstring_signature docstring = textwrap.dedent(inner.__doc__ or "").strip() # Construct :param: list @@ -144,4 +144,4 @@ def munge_docstring(f, inner): :param runner: Callable runner function or method. Should ideally be a bound method on the given context object! """ # noqa - return f"{sig}\n{docstring}\n\n{params}" + return f"{f.__name__}{sig}\n{docstring}\n\n{params}" From 38db2d2141c3136557a0fc34a8d986143668b6d4 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 19 Apr 2023 09:49:34 +0200 Subject: [PATCH 7/8] Update GitHub action Signed-off-by: Cristian Le --- .github/workflows/tests.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 994ee47..66da7ff 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,25 +18,19 @@ jobs: pytest: name: Run pytests - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest needs: [ pre-commit ] strategy: fail-fast: false matrix: - os: [ubuntu-latest] python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] - include: - - os: ubuntu-20.04 - python-version: "3.6" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install pytest - run: pip install pytest invocations mock pytest-relaxed - name: Setup patchwork - run: pip install -e . + run: pip install -e .[test] - name: Test with pytest run: pytest From a252ad319e292a06c4a81d608561767faaf933fa Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 19 Apr 2023 09:37:58 +0200 Subject: [PATCH 8/8] Add pytest for Fabric2.x Signed-off-by: Cristian Le --- .github/workflows/tests.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 66da7ff..0febad8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,6 +16,26 @@ jobs: - uses: actions/setup-python@v4 - uses: pre-commit/action@v3.0.0 + pytest-fabric2: + name: Test for Fabric 2.0 compatibility + runs-on: ubuntu-latest + needs: [ pre-commit ] + strategy: + fail-fast: false + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + fabric-version: [ "==2.2","<3.0" ] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Fabric ${{ matrix.fabric-version }} and patchwork + run: pip install "fabric${{ matrix.fabric-version }}" .[test] + - name: Test with pytest + run: pytest + pytest: name: Run pytests runs-on: ubuntu-latest