diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..0febad8 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,56 @@ +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-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 + needs: [ pre-commit ] + strategy: + fail-fast: false + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Setup patchwork + run: pip install -e .[test] + - name: Test with pytest + run: pytest 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 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 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 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 91% rename from patchwork/util.py rename to src/patchwork/util.py index 705a475..972d5c5 100644 --- a/patchwork/util.py +++ b/src/patchwork/util.py @@ -1,11 +1,10 @@ """ Helpers and decorators, primarily for internal or advanced use. """ - import textwrap from functools import wraps -from inspect import getargspec, formatargspec +from inspect import signature, Parameter # TODO: calling all functions as eg directory(c, '/foo/bar/') (with initial c) @@ -126,18 +125,16 @@ 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) + parameters = list(sig.parameters.values()) # 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]) + 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 - 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"{f.__name__}{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": {