New Upstream Release - python-plumbum

Ready changes

Summary

Merged new upstream version: 1.8.2 (was: 1.8.0).

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..2c7d170
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+  # Maintain dependencies for GitHub Actions
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"
diff --git a/.github/matchers/pylint.json b/.github/matchers/pylint.json
new file mode 100644
index 0000000..e3a6bd1
--- /dev/null
+++ b/.github/matchers/pylint.json
@@ -0,0 +1,32 @@
+{
+  "problemMatcher": [
+    {
+      "severity": "warning",
+      "pattern": [
+        {
+          "regexp": "^([^:]+):(\\d+):(\\d+): ([A-DF-Z]\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
+          "file": 1,
+          "line": 2,
+          "column": 3,
+          "code": 4,
+          "message": 5
+        }
+      ],
+      "owner": "pylint-warning"
+    },
+    {
+      "severity": "error",
+      "pattern": [
+        {
+          "regexp": "^([^:]+):(\\d+):(\\d+): (E\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
+          "file": 1,
+          "line": 2,
+          "column": 3,
+          "code": 4,
+          "message": 5
+        }
+      ],
+      "owner": "pylint-error"
+    }
+  ]
+}
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 0000000..bfdb8a7
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,38 @@
+name: CD
+
+on:
+  workflow_dispatch:
+  release:
+    types:
+    - published
+
+env:
+  FORCE_COLOR: 3
+
+jobs:
+  dist:
+    name: Dist
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+
+    - uses: hynek/build-and-inspect-python-package@v1
+
+  deploy:
+    name: Deploy
+    runs-on: ubuntu-22.04
+    needs: [dist]
+    if: github.event_name == 'release' && github.event.action == 'published'
+    environment: pypi
+    permissions:
+      id-token: write
+
+    steps:
+    - uses: actions/download-artifact@v3
+      with:
+        name: Packages
+        path: dist
+
+    - uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b22da50
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,113 @@
+name: CI
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+    - master
+    - main
+  pull_request:
+    branches:
+    - master
+    - main
+
+env:
+  FORCE_COLOR: 3
+
+jobs:
+
+  pre-commit:
+    name: Format
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+    - uses: actions/setup-python@v4
+      with:
+        python-version: "3.x"
+    - uses: pre-commit/action@v3.0.0
+    - name: pylint
+      run: |
+        echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
+        pipx run --python python nox -s pylint
+
+  tests:
+    name: Tests on 🐍 ${{ matrix.python-version }} ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.6", "3.8", "3.11", "3.12-dev"]
+        os: [ubuntu-latest, windows-latest, macos-latest]
+        include:
+        - python-version: 'pypy-3.8'
+          os: ubuntu-latest
+        - python-version: 'pypy-3.9'
+          os: ubuntu-latest
+        - python-version: '3.6'
+          os: ubuntu-20.04
+        exclude:
+        - python-version: '3.6'
+          os: ubuntu-latest
+    runs-on: ${{ matrix.os }}
+
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - uses: actions/cache@v3
+      if: runner.os == 'Linux' && startsWith(matrix.python-version, 'pypy')
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('setup.cfg') }}
+        restore-keys: |
+          ${{ runner.os }}-${{ matrix.python-version }}-pip-
+
+    - name: Install
+      run: |
+        pip install wheel coveralls pytest-github-actions-annotate-failures
+        pip install -e .[dev]
+
+    - name: Setup SSH tests
+      if: runner.os != 'Windows'
+      run: |
+        chmod 755 ~
+        mkdir -p ~/.ssh
+        chmod 755 ~/.ssh
+        echo "NoHostAuthenticationForLocalhost yes" >> ~/.ssh/config
+        echo "StrictHostKeyChecking no" >> ~/.ssh/config
+        ssh-keygen -q -f ~/.ssh/id_rsa -N ''
+        cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
+        chmod 644 ~/.ssh/authorized_keys
+        ls -la ~
+        ssh localhost -vvv "echo 'Worked!'"
+
+    - name: Test with pytest
+      run: pytest --cov --run-optional-tests=ssh,sudo
+
+    - name: Upload coverage
+      run: coveralls --service=github
+      env:
+        COVERALLS_PARALLEL: true
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        COVERALLS_FLAG_NAME: test-${{ matrix.os }}-${{ matrix.python-version }}
+
+  coverage:
+    needs: [tests]
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/setup-python@v4
+      with:
+        python-version: "3.x"
+    - name: Install coveralls
+      run: pip install coveralls
+    - name: Coveralls Finished
+      run: coveralls --service=github --finish
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5b7c904
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,169 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+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/
+cover/
+
+# Translations
+# *.mo - plubmum includes this
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# 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
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Plumbum specifics
+*.po.new
+
+/tests/nohup.out
+/plumbum/version.py
+
+# jetbrains
+.idea
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3770b2e..e418819 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,7 +5,7 @@ ci:
 repos:
 
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.3.0
+  rev: "v4.4.0"
   hooks:
   - id: check-added-large-files
   - id: check-case-conflict
@@ -20,68 +20,37 @@ repos:
   - id: trailing-whitespace
 
 - repo: https://github.com/psf/black
-  rev: "22.8.0"
+  rev: "23.3.0"
   hooks:
   - id: black
 
-- repo: https://github.com/PyCQA/isort
-  rev: "5.10.1"
+- repo: https://github.com/charliermarsh/ruff-pre-commit
+  rev: "v0.0.270"
   hooks:
-  - id: isort
-
-- repo: https://github.com/asottile/pyupgrade
-  rev: "v2.38.2"
-  hooks:
-  - id: pyupgrade
-    args: ["--py36-plus"]
-
-- repo: https://github.com/asottile/setup-cfg-fmt
-  rev: "v2.0.0"
-  hooks:
-  - id: setup-cfg-fmt
-    args: [--include-version-classifiers, --max-py-version=3.11]
-
-- repo: https://github.com/hadialqattan/pycln
-  rev: v2.1.1
-  hooks:
-  - id: pycln
-    args: [--all]
-    stages: [manual]
-
-- repo: https://github.com/pycqa/flake8
-  rev: "5.0.4"
-  hooks:
-  - id: flake8
-    exclude: docs/conf.py
-    additional_dependencies: [flake8-bugbear, flake8-print, flake8-2020]
+    - id: ruff
+      args: ["--fix", "--show-fixes"]
 
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: "v0.981"
+  rev: "v1.3.0"
   hooks:
   - id: mypy
     files: plumbum
     args: []
     additional_dependencies: [typed-ast, types-paramiko, types-setuptools]
 
-# This wants the .mo files removed
-- repo: https://github.com/mgedmin/check-manifest
-  rev: "0.48"
+- repo: https://github.com/abravalheri/validate-pyproject
+  rev: "v0.13"
   hooks:
-  - id: check-manifest
-    stages: [manual]
+  - id: validate-pyproject
 
 - repo: https://github.com/codespell-project/codespell
-  rev: v2.2.1
+  rev: "v2.2.4"
   hooks:
   - id: codespell
 
 - repo: https://github.com/pre-commit/pygrep-hooks
-  rev: "v1.9.0"
+  rev: "v1.10.0"
   hooks:
-  - id: python-check-blanket-noqa
-  - id: python-check-blanket-type-ignore
-  - id: python-no-log-warn
-  - id: python-use-type-annotations
   - id: rst-backticks
   - id: rst-directive-colons
   - id: rst-inline-touching-normal
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 79d5c07..ec29630 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,17 @@
+1.8.2
+-----
+
+* Fix author metadata on PyPI package and add static check (`#648 <https://github.com/tomerfiliba/plumbum/pull/648>`_)
+* Add testing for Python 3.12 beta 1 (`#650 <https://github.com/tomerfiliba/plumbum/pull/650>`_)
+* Use Ruff for linting (`#643 <https://github.com/tomerfiliba/plumbum/pull/643>`_)
+* Paths: Add type hinting for Path (`#646 <https://github.com/tomerfiliba/plumbum/pull/646>`_)
+
+1.8.1
+-----
+
+* Accept path-like objects (`#627 <https://github.com/tomerfiliba/plumbum/pull/627>`_)
+* Move the build backend to hatchling and hatch-vcs. Users should be unaffected. Third-party packaging may need to adapt to the new build system. (`#607 <https://github.com/tomerfiliba/plumbum/pull/607>`_)
+
 1.8.0
 -----
 
diff --git a/PKG-INFO b/PKG-INFO
index 6f86b0e..8514b2a 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,17 +1,34 @@
 Metadata-Version: 2.1
 Name: plumbum
-Version: 1.8.0
+Version: 1.8.2
 Summary: Plumbum: shell combinators library
-Home-page: https://plumbum.readthedocs.io
-Author: Tomer Filiba
-Author-email: tomerfiliba@gmail.com
-License: MIT
+Project-URL: Homepage, https://github.com/tomerfiliba/plumbum
+Project-URL: Documentation, https://plumbum.readthedocs.io/
 Project-URL: Bug Tracker, https://github.com/tomerfiliba/plumbum/issues
 Project-URL: Changelog, https://plumbum.readthedocs.io/en/latest/changelog.html
-Project-URL: Source, https://github.com/tomerfiliba/plumbum
-Keywords: path,,local,,remote,,ssh,,shell,,pipe,,popen,,process,,execution,,color,,cli
-Platform: POSIX
-Platform: Windows
+Project-URL: Cheatsheet, https://plumbum.readthedocs.io/en/latest/quickref.html
+Author-email: Tomer Filiba <tomerfiliba@gmail.com>
+License: Copyright (c) 2013 Tomer Filiba (tomerfiliba@gmail.com)
+        
+        Permission is hereby granted, free of charge, to any person obtaining a copy
+        of this software and associated documentation files (the "Software"), to deal
+        in the Software without restriction, including without limitation the rights
+        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+        copies of the Software, and to permit persons to whom the Software is
+        furnished to do so, subject to the following conditions:
+        
+        The above copyright notice and this permission notice shall be included in
+        all copies or substantial portions of the Software.
+        
+        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+        THE SOFTWARE.
+License-File: LICENSE
+Keywords: cli,color,execution,local,path,pipe,popen,process,remote,shell,ssh
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: Microsoft :: Windows
@@ -24,15 +41,24 @@ Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 Classifier: Topic :: Software Development :: Build Tools
 Classifier: Topic :: System :: Systems Administration
-Provides: plumbum
 Requires-Python: >=3.6
-Description-Content-Type: text/x-rst
+Requires-Dist: pywin32; platform_system == 'Windows' and platform_python_implementation != 'PyPy'
 Provides-Extra: dev
+Requires-Dist: paramiko; extra == 'dev'
+Requires-Dist: psutil; extra == 'dev'
+Requires-Dist: pytest-cov; extra == 'dev'
+Requires-Dist: pytest-mock; extra == 'dev'
+Requires-Dist: pytest-timeout; extra == 'dev'
+Requires-Dist: pytest>=6.0; extra == 'dev'
 Provides-Extra: docs
+Requires-Dist: sphinx-rtd-theme>=1.0.0; extra == 'docs'
+Requires-Dist: sphinx>=4.0.0; extra == 'docs'
 Provides-Extra: ssh
-License-File: LICENSE
+Requires-Dist: paramiko; extra == 'ssh'
+Description-Content-Type: text/x-rst
 
 .. image:: https://readthedocs.org/projects/plumbum/badge/
    :target: https://plumbum.readthedocs.io/en/latest/
diff --git a/conda.recipe/.gitignore b/conda.recipe/.gitignore
new file mode 100644
index 0000000..a6a2006
--- /dev/null
+++ b/conda.recipe/.gitignore
@@ -0,0 +1,6 @@
+/linux-32/*
+/linux-64/*
+/osx-64/*
+/win-32/*
+/win-64/*
+/outputdir/*
diff --git a/debian/changelog b/debian/changelog
index 687d678..583cbe6 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-plumbum (1.8.2-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 31 May 2023 21:43:49 -0000
+
 python-plumbum (1.8.0-1) unstable; urgency=medium
 
   * New upstream version 1.8.0
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..a485625
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+/_build
diff --git a/docs/_cheatsheet.rst b/docs/_cheatsheet.rst
index 26a3c8b..175925f 100644
--- a/docs/_cheatsheet.rst
+++ b/docs/_cheatsheet.rst
@@ -71,7 +71,7 @@ Working-directory manipulation
     ...
     '15\n'
 
-A more explicit, and thread-safe way of running a command in a differet directory is using the ``.with_cwd()`` method:
+A more explicit, and thread-safe way of running a command in a different directory is using the ``.with_cwd()`` method::
 
 .. code-block:: python
 
@@ -115,9 +115,9 @@ See :ref:`guide-local-commands-nesting`.
 Remote commands (over SSH)
 --------------------------
 
-Supports `openSSH <http://www.openssh.org/>`_-compatible clients,
-`PuTTY <http://www.chiark.greenend.org.uk/~sgtatham/putty/>`_ (on Windows)
-and `Paramiko <https://github.com/paramiko/paramiko/>`_ (a pure-Python implementation of SSH2)
+Supports `openSSH <https://www.openssh.com/>`_-compatible clients,
+`PuTTY <https://www.chiark.greenend.org.uk/~sgtatham/putty/>`_ (on Windows)
+and `Paramiko <https://github.com/paramiko/paramiko/>`_ (a pure-Python implementation of SSH2):
 
 .. code-block:: python
 
diff --git a/docs/_news.rst b/docs/_news.rst
index 44cda6b..dffb80f 100644
--- a/docs/_news.rst
+++ b/docs/_news.rst
@@ -1,3 +1,7 @@
+* **2023.01.01**: Version 1.8.1 released with hatchling replacing setuptools for the build system, and support for Path objects in local.
+
+* **2022.10.05**: Version 1.8.0 released with ``NO_COLOR``/``FORCE_COLOR``, ``all_markers`` & future annotations for the CLI, some command enhancements, & Python 3.11 testing.
+
 * **2021.12.23**: Version 1.7.2 released with very minor fixes, final version to support Python 2.7 and 3.5.
 
 * **2021.11.23**: Version 1.7.1 released with a few features like reverse tunnels, color group titles, and a glob path fix. Better Python 3.10 support.
diff --git a/docs/cli.rst b/docs/cli.rst
index 0e48ce5..16c9b03 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -5,7 +5,7 @@ Command-Line Interface (CLI)
 
 The other side of *executing programs* with ease is **writing CLI programs** with ease.
 Python scripts normally use ``optparse`` or the more recent ``argparse``, and their
-`derivatives <http://packages.python.org/argh/index.html>`_; but all of these are somewhat
+`derivatives <https://pythonhosted.org/argh/index.html>`_; but all of these are somewhat
 limited in their expressive power, and are quite **unintuitive** (and even **unpythonic**).
 Plumbum's CLI toolkit offers a **programmatic approach** to building command-line applications;
 instead of creating a parser object and populating it with a series of "options", the CLI toolkit
@@ -149,10 +149,10 @@ for instance, ``$ ./myapp.py --log-to-file=/tmp/log`` would translate to a call
 
 .. note::
    Methods' docstrings and argument names will be used to render the help message, keeping your
-   code as `DRY <http://en.wikipedia.org/wiki/Don't_repeat_yourself>`_ as possible.
+   code as `DRY <https://en.wikipedia.org/wiki/Don't_repeat_yourself>`_ as possible.
 
    There's also :func:`autoswitch <plumbum.cli.autoswitch>`, which infers the name of the switch
-   from the function's name, e.g. ::
+   from the function's name, e.g.::
 
         @cli.autoswitch(str)
         def log_to_file(self, filename):
@@ -163,13 +163,13 @@ for instance, ``$ ./myapp.py --log-to-file=/tmp/log`` would translate to a call
 Arguments
 ^^^^^^^^^
 As demonstrated in the example above, switch functions may take no arguments (not counting
-``self``) or a single argument argument. If a switch function accepts an argument, it must
+``self``) or a single argument. If a switch function accepts an argument, it must
 specify the argument's *type*. If you require no special validation, simply pass ``str``;
 otherwise, you may pass any type (or any callable, in fact) that will take a string and convert
 it to a meaningful object. If conversion is not possible, the type (or callable) is expected to
 raise either ``TypeError`` or ``ValueError``.
 
-For instance ::
+For instance::
 
     class MyApp(cli.Application):
         _port = 8080
@@ -194,7 +194,7 @@ The toolkit includes two additional "types" (or rather, *validators*): ``Range``
 that range (inclusive). ``Set`` takes a set of allowed values, and expects the
 argument to match one of these values. You can set ``case_sensitive=False``, or
 add ``all_markers={"*", "all"}`` if you want to have a "trigger all markers"
-marker. Here's an example ::
+marker. Here's an example::
 
     class MyApp(cli.Application):
         _port = 8080
@@ -232,7 +232,7 @@ Repeatable Switches
 Many times, you would like to allow a certain switch to be given multiple times. For instance,
 in ``gcc``, you may give several include directories using ``-I``. By default, switches may
 only be given once, unless you allow multiple occurrences by passing ``list = True`` to the
-``switch`` decorator ::
+``switch`` decorator::
 
     class MyApp(cli.Application):
         _dirs = []
@@ -260,7 +260,7 @@ for this switch.
 
 Dependencies
 ^^^^^^^^^^^^
-Many time, the occurrence of a certain switch depends on the occurrence of another, e..g, it
+Many times, the occurrence of a certain switch depends on the occurrence of another, e.g., it
 may not be possible to give ``-x`` without also giving ``-y``. This constraint can be achieved
 by specifying the ``requires`` keyword argument to the ``switch`` decorator; it is a list
 of switch names that this switch depends on. If the required switches are missing, the user
@@ -322,10 +322,9 @@ Switch Attributes
 Many times it's desired to simply store a switch's argument in an attribute, or set a flag if
 a certain switch is given. For this purpose, the toolkit provides
 :class:`SwitchAttr <plumbum.cli.SwitchAttr>`, which is `data descriptor
-<http://docs.python.org/howto/descriptor.html>`_ that stores the argument in an instance attribute.
+<https://docs.python.org/howto/descriptor.html>`_ that stores the argument in an instance attribute.
 There are two additional "flavors" of ``SwitchAttr``: ``Flag`` (which toggles its default value
-if the switch is given) and ``CountOf`` (which counts the number of occurrences of the switch)
-::
+if the switch is given) and ``CountOf`` (which counts the number of occurrences of the switch)::
 
     class MyApp(cli.Application):
         log_file = cli.SwitchAttr("--log-file", str, default = None)
@@ -372,7 +371,7 @@ It may take any number of *positional argument*; for instance, in ``cp -r /foo /
 that the program would accept depends on the signature of the method: if the method takes 5
 arguments, 2 of which have default values, then at least 3 positional arguments must be supplied
 by the user and at most 5. If the method also takes varargs (``*args``), the number of
-arguments that may be given is unbound ::
+arguments that may be given is unbound::
 
     class MyApp(cli.Application):
         def main(self, src, dst, mode = "normal"):
@@ -462,7 +461,7 @@ Sub-commands
 
 A common practice of CLI applications, as they span out and get larger, is to split their
 logic into multiple, pluggable *sub-applications* (or *sub-commands*). A classic example is version
-control systems, such as `git <http://git-scm.com/>`_, where ``git`` is the *root* command,
+control systems, such as `git <https://git-scm.com/>`_, where ``git`` is the *root* command,
 under which sub-commands such as ``commit`` or ``push`` are nested. Git even supports ``alias``-ing,
 which creates allows users to create custom sub-commands. Plumbum makes writing such applications
 really easy.
@@ -485,7 +484,7 @@ Before we get to the code, it is important to stress out two things:
   is normally used.
 
 Here is an example of a mock version control system, called ``geet``. We're going to have a root
-application ``Geet``, which has two sub-commands - ``GeetCommit`` and ``GeetPush``: these are
+application ``Geet``, which has two sub-commands – ``GeetCommit`` and ``GeetPush``: these are
 attached to the root application using the ``subcommand`` decorator ::
 
     class Geet(cli.Application):
@@ -617,9 +616,9 @@ For the full list of helpers or more information, see the :ref:`api docs <api-cl
 See Also
 --------
 * `filecopy.py <https://github.com/tomerfiliba/plumbum/blob/master/examples/filecopy.py>`_ example
-* `geet.py <https://github.com/tomerfiliba/plumbum/blob/master/examples/geet.py>`_ - a runnable
+* `geet.py <https://github.com/tomerfiliba/plumbum/blob/master/examples/geet.py>`_ – a runnable
   example of using sub-commands
-* `RPyC <http://rpyc.sf.net>`_ has changed it bash-based build script to Plumbum CLI.
+* `RPyC <https://rpyc.readthedocs.io/>`_ has changed its bash-based build script to Plumbum CLI.
   Notice `how short and readable <https://github.com/tomerfiliba/rpyc/blob/c457a28d689df7605838334a437c6b35f9a94618/build.py>`_
   it is.
 * A `blog post <http://tomerfiliba.com/blog/Plumbum/>`_ describing the philosophy of the CLI module
diff --git a/docs/index.rst b/docs/index.rst
index b0294c2..78bf283 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -10,12 +10,12 @@
     <li><a href="#about" title="Jump to user guide">About</a></li>
     </ul>
     <hr/>
-    <a href="http://tomerfiliba.com" target="_blank">
+    <a href="https://tomerfiliba.com" target="_blank">
     <img style="display: block; margin-left: auto; margin-right: auto" alt="Tomer Filiba"
     src="_static/fish-text-black.png" title="Tomer's Blog"/>
     <span style="color:transparent;position: absolute;font-size:5px;width: 0px;height: 0px;">Tomer Filiba</span></a>
     <br/>
-    <a href="http://github.com/tomerfiliba/plumbum" target="_blank">
+    <a href="https://github.com/tomerfiliba/plumbum" target="_blank">
     <img style="display: block; margin-left: auto; margin-right: auto; opacity: 0.7; width: 70px;"
     src="_static/github-logo.png" title="Github Repo"/></a>
     <br/>
@@ -34,7 +34,7 @@ Plumbum: Shell Combinators and More
 
    <strong>Sticky</strong><br/>
 
-   <a class="reference external" href="https://pypi.python.org/pypi/rpyc">Version 3.2.3</a>
+   <a class="reference external" href="https://pypi.org/project/rpyc">Version 3.2.3</a>
    was released on December 2nd <br/>
 
    Please use the
@@ -75,7 +75,7 @@ Development and Installation
 ============================
 
 The library is developed on `GitHub <https://github.com/tomerfiliba/plumbum>`_, and will happily
-accept `patches <http://help.github.com/send-pull-requests/>`_ from users. Please use the GitHub's
+accept `patches <https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request>`_ from users. Please use the GitHub's
 built-in `issue tracker <https://github.com/tomerfiliba/plumbum/issues>`_ to report any problem
 you encounter or to request features. The library is released under the permissive `MIT license
 <https://github.com/tomerfiliba/plumbum/blob/master/LICENSE>`_.
@@ -87,10 +87,10 @@ Plumbum supports **Python 3.6-3.10** and **PyPy** and is continually tested on
 **Linux**, **Mac**, and **Windows** machines through `GitHub Actions
 <https://github.com/tomerfiliba/plumbum/actions>`_.  Any Unix-like machine
 should work fine out of the box, but on Windows, you'll probably want to
-install a decent `coreutils <http://en.wikipedia.org/wiki/Coreutils>`_
+install a decent `coreutils <https://en.wikipedia.org/wiki/GNU_Core_Utilities/>`_
 environment and add it to your ``PATH``, or use WSL(2). I can recommend `mingw
-<http://mingw.org/>`_ (which comes bundled with `Git for Windows
-<http://msysgit.github.com/>`_), but `cygwin <http://www.cygwin.com/>`_ should
+<https://mingw.osdn.io/>`_ (which comes bundled with `Git for Windows
+<https://gitforwindows.org/>`_), but `cygwin <http://www.cygwin.com/>`_ should
 work too. If you only wish to use Plumbum as a Popen-replacement to run Windows
 programs, then there's no need for the Unix tools.
 
@@ -106,7 +106,7 @@ Download
 --------
 
 You can **download** the library from the `Python Package Index
-<http://pypi.python.org/pypi/plumbum#downloads>`_ (in a variety of formats), or
+<https://pypi.org/pypi/plumbum/#files>`_ (in a variety of formats), or
 run ``pip install plumbum`` directly. If you use Anaconda, you can also get it
 from the ``conda-forge`` channel with ``conda install -c conda-forge plumbum``.
 
@@ -161,12 +161,12 @@ I've toyed with this idea for some time now, but it wasn't until I had to write
 for a project I've been working on that I decided I've had it with shell scripts and it's time
 to make it happen. Plumbum was born from the scraps of the ``Path`` class, which I
 wrote for the aforementioned build system, and the ``SshContext`` and ``SshTunnel`` classes
-that I wrote for `RPyC <http://rpyc.sf.net>`_. When I combined the two with *shell combinators*
+that I wrote for `RPyC <https://rpyc.readthedocs.io/>`_. When I combined the two with *shell combinators*
 (because shell scripts do have an edge there) the magic happened and here we are.
 
 Credits
 =======
-The project has been inspired by **PBS** (now called `sh <http://amoffat.github.com/sh/>`_)
+The project has been inspired by **PBS** (now called `sh <http://amoffat.github.io/sh/>`_)
 of `Andrew Moffat <https://github.com/amoffat>`_,
 and has borrowed some of his ideas (namely treating programs like functions and the
 nice trick for importing commands). However, I felt there was too much magic going on in PBS,
diff --git a/docs/local_commands.rst b/docs/local_commands.rst
index 39b4370..4064b83 100644
--- a/docs/local_commands.rst
+++ b/docs/local_commands.rst
@@ -99,6 +99,15 @@ using ``|`` (bitwise-or)::
     >>> chain()
     '-rw-r--r--    1 sebulba  Administ        0 Apr 27 11:54 setup.py\n'
 
+.. note::
+  Unlike common posix shells, plumbum only captures stderr of the last command in a pipeline.
+  If any of the other commands writes a large amount of text to the stderr, the whole pipeline
+  will stall (large amount equals to >64k on posix systems). This can happen with bioinformatics
+  tools that write progress information to stderr. To avoid this issue, you can discard stderr
+  of the first commands or redirect it to a file.
+
+  >>> chain = (bwa["mem", ...] >= "/dev/null") | samtools["view", ...]
+
 .. _guide-local-commands-redir:
 
 Input/Output Redirection
diff --git a/examples/.gitignore b/examples/.gitignore
new file mode 100644
index 0000000..72eec0d
--- /dev/null
+++ b/examples/.gitignore
@@ -0,0 +1,5 @@
+testfigure.pdf
+testfigure.svg
+testfigure.log
+testfigure.aux
+testfigure.png
diff --git a/examples/filecopy.py b/examples/filecopy.py
index 0ab6635..6efc4f2 100755
--- a/examples/filecopy.py
+++ b/examples/filecopy.py
@@ -26,8 +26,7 @@ class FileCopier(cli.Application):
             if not self.overwrite:
                 logger.debug("Oh no! That's terrible")
                 raise ValueError("Destination already exists")
-            else:
-                delete(dst)
+            delete(dst)
 
         logger.debug("I'm going to copy %s to %s", src, dst)
         copy(src, dst)
diff --git a/experiments/parallel.py b/experiments/parallel.py
index c977ad0..9954efd 100644
--- a/experiments/parallel.py
+++ b/experiments/parallel.py
@@ -5,17 +5,19 @@ from plumbum.commands.processes import CommandNotFound, ProcessExecutionError, r
 def make_concurrent(self, rhs):
     if not isinstance(rhs, BaseCommand):
         raise TypeError("rhs must be an instance of BaseCommand")
+
     if isinstance(self, ConcurrentCommand):
         if isinstance(rhs, ConcurrentCommand):
             self.commands.extend(rhs.commands)
         else:
             self.commands.append(rhs)
         return self
-    elif isinstance(rhs, ConcurrentCommand):
+
+    if isinstance(rhs, ConcurrentCommand):
         rhs.commands.insert(0, self)
         return rhs
-    else:
-        return ConcurrentCommand(self, rhs)
+
+    return ConcurrentCommand(self, rhs)
 
 
 BaseCommand.__and__ = make_concurrent
@@ -69,7 +71,7 @@ class ConcurrentCommand(BaseCommand):
         for cmd in self.commands:
             form.extend(cmd.formulate(level, args))
             form.append("&")
-        return form + [")"]
+        return [*form, ")"]
 
     def popen(self, *args, **kwargs):
         return ConcurrentPopen([cmd[args].popen(**kwargs) for cmd in self.commands])
@@ -82,8 +84,8 @@ class ConcurrentCommand(BaseCommand):
             ]
         if not args:
             return self
-        else:
-            return ConcurrentCommand(*(cmd[args] for cmd in self.commands))
+
+        return ConcurrentCommand(*(cmd[args] for cmd in self.commands))
 
 
 class Cluster:
@@ -164,7 +166,7 @@ class ClusterSession:
         self.close()
 
     def __del__(self):
-        try:
+        try:  # noqa: 167
             self.close()
         except Exception:
             pass
diff --git a/noxfile.py b/noxfile.py
index fa6be3b..9c39283 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import nox
 
-ALL_PYTHONS = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
+ALL_PYTHONS = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
 
 nox.options.sessions = ["lint", "tests"]
 
@@ -22,7 +22,7 @@ def pylint(session):
     Run pylint.
     """
 
-    session.install(".", "paramiko", "ipython", "pylint~=2.14.3")
+    session.install(".", "paramiko", "ipython", "pylint~=2.17.4")
     session.run("pylint", "plumbum", *session.posargs)
 
 
diff --git a/plumbum.egg-info/PKG-INFO b/plumbum.egg-info/PKG-INFO
deleted file mode 100644
index 6f86b0e..0000000
--- a/plumbum.egg-info/PKG-INFO
+++ /dev/null
@@ -1,231 +0,0 @@
-Metadata-Version: 2.1
-Name: plumbum
-Version: 1.8.0
-Summary: Plumbum: shell combinators library
-Home-page: https://plumbum.readthedocs.io
-Author: Tomer Filiba
-Author-email: tomerfiliba@gmail.com
-License: MIT
-Project-URL: Bug Tracker, https://github.com/tomerfiliba/plumbum/issues
-Project-URL: Changelog, https://plumbum.readthedocs.io/en/latest/changelog.html
-Project-URL: Source, https://github.com/tomerfiliba/plumbum
-Keywords: path,,local,,remote,,ssh,,shell,,pipe,,popen,,process,,execution,,color,,cli
-Platform: POSIX
-Platform: Windows
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Topic :: Software Development :: Build Tools
-Classifier: Topic :: System :: Systems Administration
-Provides: plumbum
-Requires-Python: >=3.6
-Description-Content-Type: text/x-rst
-Provides-Extra: dev
-Provides-Extra: docs
-Provides-Extra: ssh
-License-File: LICENSE
-
-.. image:: https://readthedocs.org/projects/plumbum/badge/
-   :target: https://plumbum.readthedocs.io/en/latest/
-   :alt: Documentation Status
-.. image:: https://github.com/tomerfiliba/plumbum/workflows/CI/badge.svg
-   :target: https://github.com/tomerfiliba/plumbum/actions
-   :alt: Build Status
-.. image:: https://coveralls.io/repos/tomerfiliba/plumbum/badge.svg?branch=master&service=github
-   :target: https://coveralls.io/github/tomerfiliba/plumbum?branch=master
-   :alt: Coverage Status
-.. image:: https://img.shields.io/pypi/v/plumbum.svg
-   :target: https://pypi.python.org/pypi/plumbum/
-   :alt: PyPI Status
-.. image:: https://img.shields.io/pypi/pyversions/plumbum.svg
-   :target: https://pypi.python.org/pypi/plumbum/
-   :alt: PyPI Versions
-.. image:: https://img.shields.io/conda/vn/conda-forge/plumbum.svg
-   :target: https://github.com/conda-forge/plumbum-feedstock
-   :alt: Conda-Forge Badge
-.. image:: https://img.shields.io/pypi/l/plumbum.svg
-   :target: https://pypi.python.org/pypi/plumbum/
-   :alt: PyPI License
-.. image:: https://badges.gitter.im/plumbumpy/Lobby.svg
-   :alt: Join the chat at https://gitter.im/plumbumpy/Lobby
-   :target: https://gitter.im/plumbumpy/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-   :alt: Code styled with Black
-   :target: https://github.com/psf/black
-
-
-Plumbum: Shell Combinators
-==========================
-
-Ever wished the compactness of shell scripts be put into a **real** programming language?
-Say hello to *Plumbum Shell Combinators*. Plumbum (Latin for *lead*, which was used to create
-pipes back in the day) is a small yet feature-rich library for shell script-like programs in Python.
-The motto of the library is **"Never write shell scripts again"**, and thus it attempts to mimic
-the **shell syntax** ("shell combinators") where it makes sense, while keeping it all **Pythonic
-and cross-platform**.
-
-Apart from shell-like syntax and handy shortcuts, the library provides local and remote command
-execution (over SSH), local and remote file-system paths, easy working-directory and environment
-manipulation, and a programmatic Command-Line Interface (CLI) application toolkit.
-Now let's see some code!
-
-*This is only a teaser; the full documentation can be found at*
-`Read the Docs <https://plumbum.readthedocs.io>`_
-
-Cheat Sheet
------------
-
-Basics
-******
-
-.. code-block:: python
-
-    >>> from plumbum import local
-    >>> local.cmd.ls
-    LocalCommand(/bin/ls)
-    >>> local.cmd.ls()
-    'build.py\nCHANGELOG.rst\nconda.recipe\nCONTRIBUTING.rst\ndocs\nexamples\nexperiments\nLICENSE\nMANIFEST.in\nPipfile\nplumbum\nplumbum.egg-info\npytest.ini\nREADME.rst\nsetup.cfg\nsetup.py\ntests\ntranslations.py\n'
-    >>> notepad = local["c:\\windows\\notepad.exe"]
-    >>> notepad()                                   # Notepad window pops up
-    ''                                              # Notepad window is closed by user, command returns
-
-In the example above, you can use ``local["ls"]`` if you have an unusually named executable or a full path to an executable. The ``local`` object represents your local machine. As you'll see, Plumbum also provides remote machines that use the same API!
-You can also use ``from plumbum.cmd import ls`` as well for accessing programs in the ``PATH``.
-
-Piping
-******
-
-.. code-block:: python
-
-    >>> from plumbum.cmd import ls, grep, wc
-    >>> chain = ls["-a"] | grep["-v", r"\.py"] | wc["-l"]
-    >>> print(chain)
-    /bin/ls -a | /bin/grep -v '\.py' | /usr/bin/wc -l
-    >>> chain()
-    '27\n'
-
-Redirection
-***********
-
-.. code-block:: python
-
-    >>> from plumbum.cmd import cat, head
-    >>> ((cat < "setup.py") | head["-n", 4])()
-    '#!/usr/bin/env python3\nimport os\n\ntry:\n'
-    >>> (ls["-a"] > "file.list")()
-    ''
-    >>> (cat["file.list"] | wc["-l"])()
-    '31\n'
-
-Working-directory manipulation
-******************************
-
-.. code-block:: python
-
-    >>> local.cwd
-    <LocalWorkdir /home/tomer/workspace/plumbum>
-    >>> with local.cwd(local.cwd / "docs"):
-    ...     chain()
-    ...
-    '22\n'
-
-Foreground and background execution
-***********************************
-
-.. code-block:: python
-
-    >>> from plumbum import FG, BG
-    >>> (ls["-a"] | grep[r"\.py"]) & FG         # The output is printed to stdout directly
-    build.py
-    setup.py
-    translations.py
-    >>> (ls["-a"] | grep[r"\.py"]) & BG         # The process runs "in the background"
-    <Future ['/bin/grep', '\\.py'] (running)>
-
-Command nesting
-***************
-
-.. code-block:: python
-
-    >>> from plumbum.cmd import sudo, ifconfig
-    >>> print(sudo[ifconfig["-a"]])
-    /usr/bin/sudo /sbin/ifconfig -a
-    >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG
-    lo        Link encap:Local Loopback
-              UP LOOPBACK RUNNING  MTU:16436  Metric:1
-
-Remote commands (over SSH)
-**************************
-
-Supports `openSSH <http://www.openssh.org/>`_-compatible clients,
-`PuTTY <http://www.chiark.greenend.org.uk/~sgtatham/putty/>`_ (on Windows)
-and `Paramiko <https://github.com/paramiko/paramiko/>`_ (a pure-Python implementation of SSH2)
-
-.. code-block:: python
-
-    >>> from plumbum import SshMachine
-    >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa")
-    >>> r_ls = remote["ls"]
-    >>> with remote.cwd("/lib"):
-    ...     (r_ls | grep["0.so.0"])()
-    ...
-    'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n'
-
-CLI applications
-****************
-
-.. code-block:: python
-
-    import logging
-    from plumbum import cli
-
-    class MyCompiler(cli.Application):
-        verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode")
-        include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories")
-
-        @cli.switch("--loglevel", int)
-        def set_log_level(self, level):
-            """Sets the log-level of the logger"""
-            logging.root.setLevel(level)
-
-        def main(self, *srcfiles):
-            print("Verbose:", self.verbose)
-            print("Include dirs:", self.include_dirs)
-            print("Compiling:", srcfiles)
-
-    if __name__ == "__main__":
-        MyCompiler.run()
-
-Sample output
-+++++++++++++
-
-::
-
-    $ python3 simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp
-    Verbose: True
-    Include dirs: ['foo/bar', 'spam/eggs']
-    Compiling: ('x.cpp', 'y.cpp', 'z.cpp')
-
-Colors and Styles
------------------
-
-.. code-block:: python
-
-    from plumbum import colors
-    with colors.red:
-        print("This library provides safe, flexible color access.")
-        print(colors.bold | "(and styles in general)", "are easy!")
-    print("The simple 16 colors or",
-          colors.orchid & colors.underline | '256 named colors,',
-          colors.rgb(18, 146, 64) | "or full rgb colors",
-          'can be used.')
-    print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.")
diff --git a/plumbum.egg-info/SOURCES.txt b/plumbum.egg-info/SOURCES.txt
deleted file mode 100644
index d9b6474..0000000
--- a/plumbum.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,152 +0,0 @@
-.editorconfig
-.gitattributes
-.gitignore
-.pre-commit-config.yaml
-.readthedocs.yml
-CHANGELOG.rst
-CONTRIBUTING.rst
-LICENSE
-MANIFEST.in
-README.rst
-noxfile.py
-pyproject.toml
-setup.cfg
-setup.py
-translations.py
-.github/dependabot.yml
-.github/matchers/pylint.json
-.github/workflows/ci.yml
-conda.recipe/.gitignore
-conda.recipe/README.mkd
-conda.recipe/bld.bat
-conda.recipe/build.sh
-conda.recipe/meta.yaml
-docs/.gitignore
-docs/Makefile
-docs/_cheatsheet.rst
-docs/_color_list.html
-docs/_news.rst
-docs/changelog.rst
-docs/cli.rst
-docs/colorlib.rst
-docs/colors.rst
-docs/conf.py
-docs/index.rst
-docs/local_commands.rst
-docs/local_machine.rst
-docs/make.bat
-docs/paths.rst
-docs/quickref.rst
-docs/remote.rst
-docs/typed_env.rst
-docs/utils.rst
-docs/_static/fish-text-black.png
-docs/_static/github-logo.png
-docs/_static/logo.png
-docs/_static/logo2.png
-docs/_static/logo3.png
-docs/_static/logo4.png
-docs/_static/logo6.png
-docs/_static/logo7.png
-docs/_static/logo8.png
-docs/_static/placeholder
-docs/_templates/placeholder
-docs/api/cli.rst
-docs/api/colors.rst
-docs/api/commands.rst
-docs/api/fs.rst
-docs/api/machines.rst
-docs/api/path.rst
-examples/.gitignore
-examples/PHSP.png
-examples/SimpleColorCLI.py
-examples/alignment.py
-examples/color.py
-examples/filecopy.py
-examples/fullcolor.py
-examples/geet.py
-examples/make_figures.py
-examples/simple_cli.py
-examples/testfigure.tex
-experiments/parallel.py
-experiments/test_parallel.py
-plumbum/__init__.py
-plumbum/_testtools.py
-plumbum/cmd.py
-plumbum/colors.py
-plumbum/lib.py
-plumbum/typed_env.py
-plumbum/version.py
-plumbum.egg-info/PKG-INFO
-plumbum.egg-info/SOURCES.txt
-plumbum.egg-info/dependency_links.txt
-plumbum.egg-info/requires.txt
-plumbum.egg-info/top_level.txt
-plumbum/cli/__init__.py
-plumbum/cli/application.py
-plumbum/cli/config.py
-plumbum/cli/i18n.py
-plumbum/cli/image.py
-plumbum/cli/progress.py
-plumbum/cli/switches.py
-plumbum/cli/terminal.py
-plumbum/cli/termsize.py
-plumbum/cli/i18n/de.po
-plumbum/cli/i18n/fr.po
-plumbum/cli/i18n/nl.po
-plumbum/cli/i18n/ru.po
-plumbum/cli/i18n/de/LC_MESSAGES/plumbum.cli.mo
-plumbum/cli/i18n/fr/LC_MESSAGES/plumbum.cli.mo
-plumbum/cli/i18n/nl/LC_MESSAGES/plumbum.cli.mo
-plumbum/cli/i18n/ru/LC_MESSAGES/plumbum.cli.mo
-plumbum/colorlib/__init__.py
-plumbum/colorlib/__main__.py
-plumbum/colorlib/_ipython_ext.py
-plumbum/colorlib/factories.py
-plumbum/colorlib/names.py
-plumbum/colorlib/styles.py
-plumbum/commands/__init__.py
-plumbum/commands/base.py
-plumbum/commands/daemons.py
-plumbum/commands/modifiers.py
-plumbum/commands/processes.py
-plumbum/fs/__init__.py
-plumbum/fs/atomic.py
-plumbum/fs/mounts.py
-plumbum/machines/__init__.py
-plumbum/machines/_windows.py
-plumbum/machines/base.py
-plumbum/machines/env.py
-plumbum/machines/local.py
-plumbum/machines/paramiko_machine.py
-plumbum/machines/remote.py
-plumbum/machines/session.py
-plumbum/machines/ssh_machine.py
-plumbum/path/__init__.py
-plumbum/path/base.py
-plumbum/path/local.py
-plumbum/path/remote.py
-plumbum/path/utils.py
-tests/_test_paramiko.py
-tests/conftest.py
-tests/env.py
-tests/file with space.txt
-tests/slow_process.bash
-tests/test_3_cli.py
-tests/test_cli.py
-tests/test_clicolor.py
-tests/test_color.py
-tests/test_config.py
-tests/test_env.py
-tests/test_factories.py
-tests/test_local.py
-tests/test_nohup.py
-tests/test_putty.py
-tests/test_remote.py
-tests/test_sudo.py
-tests/test_terminal.py
-tests/test_typed_env.py
-tests/test_utils.py
-tests/test_validate.py
-tests/test_visual_color.py
-tests/not-in-path/dummy-executable
\ No newline at end of file
diff --git a/plumbum.egg-info/dependency_links.txt b/plumbum.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/plumbum.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/plumbum.egg-info/requires.txt b/plumbum.egg-info/requires.txt
deleted file mode 100644
index 801cb16..0000000
--- a/plumbum.egg-info/requires.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-
-[:platform_system == "Windows" and platform_python_implementation != "PyPy"]
-pywin32
-
-[dev]
-paramiko
-psutil
-pytest>=6.0
-pytest-cov
-pytest-mock
-pytest-timeout
-
-[docs]
-Sphinx>=4.0.0
-sphinx-rtd-theme>=1.0.0
-
-[ssh]
-paramiko
diff --git a/plumbum.egg-info/top_level.txt b/plumbum.egg-info/top_level.txt
deleted file mode 100644
index ccf9348..0000000
--- a/plumbum.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-plumbum
diff --git a/plumbum/cli/application.py b/plumbum/cli/application.py
index d83e6f1..6e74363 100644
--- a/plumbum/cli/application.py
+++ b/plumbum/cli/application.py
@@ -320,7 +320,7 @@ class Application:
                 subcmd = self._subcommands[a].get()
                 self.nested_command = (
                     subcmd,
-                    [self.PROGNAME + " " + self._subcommands[a].name] + argv,
+                    [self.PROGNAME + " " + self._subcommands[a].name, *argv],
                 )
                 break
 
@@ -549,7 +549,7 @@ class Application:
                     else sig.return_annotation
                 )
                 if sys.version_info < (3, 10) and isinstance(annotation, str):
-                    annotation = eval(annotation)
+                    annotation = eval(annotation)  # noqa: PGH001
                 if item == m.varargs:
                     varargs = annotation
                 elif item != "return":
@@ -570,7 +570,6 @@ class Application:
         out_args = list(args)
 
         for i in range(min(len(args), len(validator_list))):
-
             if validator_list[i] is not None:
                 out_args[i] = self._handle_argument(
                     args[i], validator_list[i], argnames[i]
diff --git a/plumbum/cli/i18n.py b/plumbum/cli/i18n.py
index 915231a..933b278 100644
--- a/plumbum/cli/i18n.py
+++ b/plumbum/cli/i18n.py
@@ -15,7 +15,7 @@ if loc is None or loc.startswith("en"):
             return strN.replace("{0}", str(n))
 
     def get_translation_for(
-        package_name: str,  # pylint: disable=unused-argument
+        package_name: str,  # noqa: ARG001
     ) -> NullTranslation:
         return NullTranslation()
 
diff --git a/plumbum/cli/image.py b/plumbum/cli/image.py
index 08c6d82..da022b3 100644
--- a/plumbum/cli/image.py
+++ b/plumbum/cli/image.py
@@ -7,7 +7,6 @@ from .termsize import get_terminal_size
 
 
 class Image:
-
     __slots__ = "size char_ratio".split()
 
     def __init__(self, size=None, char_ratio=2.45):
@@ -26,10 +25,9 @@ class Image:
         orig_ratio = orig[0] / orig[1] / self.char_ratio
 
         if int(term[1] / orig_ratio) <= term[0]:
-            new_size = int(term[1] / orig_ratio), term[1]
-        else:
-            new_size = term[0], int(term[0] * orig_ratio)
-        return new_size
+            return int(term[1] / orig_ratio), term[1]
+
+        return term[0], int(term[0] * orig_ratio)
 
     def show(self, filename, double=False):
         """Display an image on the command line. Can select a size or show in double resolution."""
@@ -100,7 +98,6 @@ class ShowImageApp(cli.Application):
 
     @cli.positional(cli.ExistingFile)
     def main(self, filename):
-
         size = None
         if self.size:
             size = map(int, self.size.split("x"))
diff --git a/plumbum/cli/progress.py b/plumbum/cli/progress.py
index 85513d1..ddfa2cc 100644
--- a/plumbum/cli/progress.py
+++ b/plumbum/cli/progress.py
@@ -187,7 +187,6 @@ class ProgressIPy(ProgressBase):  # pragma: no cover
     HTMLBOX = '<div class="widget-hbox widget-progress"><div class="widget-label" style="display:block;">{0}</div></div>'
 
     def __init__(self, *args, **kargs):
-
         # Ipython gives warnings when using widgets about the API potentially changing
         with warnings.catch_warnings():
             warnings.simplefilter("ignore")
@@ -242,7 +241,7 @@ class ProgressAuto(ProgressBase):
     def __new__(cls, *args, **kargs):
         """Uses the generator trick that if a cls instance is returned, the __init__ method is not called."""
         try:  # pragma: no cover
-            __IPYTHON__  # pylint: disable=pointless-statement
+            __IPYTHON__  # noqa: B018
             try:
                 from traitlets import TraitError
             except ImportError:  # Support for IPython < 4.0
diff --git a/plumbum/cli/switches.py b/plumbum/cli/switches.py
index 692b599..437520b 100644
--- a/plumbum/cli/switches.py
+++ b/plumbum/cli/switches.py
@@ -161,10 +161,7 @@ def switch(
     def deco(func):
         if argname is None:
             argspec = inspect.getfullargspec(func).args
-            if len(argspec) == 2:
-                argname2 = argspec[1]
-            else:
-                argname2 = _("VALUE")
+            argname2 = argspec[1] if len(argspec) == 2 else _("VALUE")
         else:
             argname2 = argname
         help2 = getdoc(func) if help is None else help
@@ -389,7 +386,8 @@ class Validator(ABC):
     def __call__(self, obj):
         "Must be implemented for a Validator to work"
 
-    def choices(self, partial=""):  # pylint: disable=no-self-use, unused-argument
+    # pylint: disable-next=no-self-use
+    def choices(self, partial=""):  # noqa: ARG002
         """Should return set of valid choices, can be given optional partial info"""
         return set()
 
@@ -438,7 +436,7 @@ class Range(Validator):
             )
         return obj
 
-    def choices(self, partial=""):
+    def choices(self, partial=""):  # noqa: ARG002
         # TODO: Add partial handling
         return set(range(self.start, self.end + 1))
 
@@ -495,7 +493,7 @@ class Set(Validator):
         for opt in self.values:
             if isinstance(opt, str):
                 if not self.case_sensitive:
-                    opt = opt.lower()
+                    opt = opt.lower()  # noqa: PLW2901
                 if opt == value or value in self.all_markers:
                     yield opt  # always return original value
                 continue
@@ -515,7 +513,7 @@ class Set(Validator):
         choices = {opt if isinstance(opt, str) else f"({opt})" for opt in self.values}
         choices |= self.all_markers
         if partial:
-            choices = {opt for opt in choices if opt.lower().startswith(partial)}
+            return {opt for opt in choices if opt.lower().startswith(partial)}
         return choices
 
 
@@ -534,7 +532,8 @@ class Predicate:
     def __call__(self, val):
         return self.func(val)
 
-    def choices(self, partial=""):  # pylint: disable=no-self-use, unused-argument
+    # pylint: disable-next=no-self-use
+    def choices(self, partial=""):  # noqa: ARG002
         return set()
 
 
diff --git a/plumbum/cli/terminal.py b/plumbum/cli/terminal.py
index 5c506d6..6ce4165 100644
--- a/plumbum/cli/terminal.py
+++ b/plumbum/cli/terminal.py
@@ -93,8 +93,7 @@ def choose(question, options, default=None):
     sys.stdout.write(question.rstrip() + "\n")
     choices = {}
     defindex = None
-    for i, item in enumerate(options):
-        i += 1
+    for i, item in enumerate(options, 1):
         if isinstance(item, (tuple, list)) and len(item) == 2:
             text = item[0]
             val = item[1]
@@ -106,10 +105,7 @@ def choose(question, options, default=None):
             defindex = i
         sys.stdout.write(f"({i}) {text}\n")
     if default is not None:
-        if defindex is None:
-            msg = f"Choice [{default}]: "
-        else:
-            msg = f"Choice [{defindex}]: "
+        msg = f"Choice [{default}]: " if defindex is None else f"Choice [{defindex}]: "
     else:
         msg = "Choice: "
     while True:
@@ -133,7 +129,7 @@ def prompt(
     question,
     type=str,  # pylint: disable=redefined-builtin
     default=NotImplemented,
-    validator=lambda val: True,
+    validator=lambda _: True,
 ):
     """
     Presents the user with a validated question, keeps asking if validation does not pass.
diff --git a/plumbum/cli/termsize.py b/plumbum/cli/termsize.py
index d56e569..55dae19 100644
--- a/plumbum/cli/termsize.py
+++ b/plumbum/cli/termsize.py
@@ -33,14 +33,14 @@ def get_terminal_size(default: Tuple[int, int] = (80, 25)) -> Tuple[int, int]:
 
     else:  # pragma: no cover
         warnings.warn(
-            "Plumbum does not know the type of the current OS for term size, defaulting to UNIX"
+            "Plumbum does not know the type of the current OS for term size, defaulting to UNIX",
+            stacklevel=2,
         )
         size = _get_terminal_size_linux()
 
-    if (
-        size is None
-    ):  # we'll assume the standard 80x25 if for any reason we don't know the terminal size
-        size = default
+    # we'll assume the standard 80x25 if for any reason we don't know the terminal size
+    if size is None:
+        return default
     return size
 
 
diff --git a/plumbum/colorlib/_ipython_ext.py b/plumbum/colorlib/_ipython_ext.py
index f8ff2c3..37f4158 100644
--- a/plumbum/colorlib/_ipython_ext.py
+++ b/plumbum/colorlib/_ipython_ext.py
@@ -4,7 +4,7 @@ from io import StringIO
 import IPython.display
 from IPython.core.magic import Magics, cell_magic, magics_class, needs_local_scope
 
-valid_choices = [x[8:] for x in dir(IPython.display) if "display_" == x[:8]]
+valid_choices = [x[8:] for x in dir(IPython.display) if x[:8] == "display_"]
 
 
 @magics_class
diff --git a/plumbum/colorlib/names.py b/plumbum/colorlib/names.py
index 6128855..cb3a590 100644
--- a/plumbum/colorlib/names.py
+++ b/plumbum/colorlib/names.py
@@ -318,25 +318,25 @@ color_codes_simple = list(range(8)) + list(range(60, 68))
 """Simple colors, remember that reset is #9, second half is non as common."""
 
 # Attributes
-attributes_ansi = dict(
-    bold=1,
-    dim=2,
-    italics=3,
-    underline=4,
-    reverse=7,
-    hidden=8,
-    strikeout=9,
-)
+attributes_ansi = {
+    "bold": 1,
+    "dim": 2,
+    "italics": 3,
+    "underline": 4,
+    "reverse": 7,
+    "hidden": 8,
+    "strikeout": 9,
+}
 
 # Stylesheet
-default_styles = dict(
-    warn="fg red",
-    title="fg cyan underline bold",
-    fatal="fg red bold",
-    highlight="bg yellow",
-    info="fg blue",
-    success="fg green",
-)
+default_styles = {
+    "warn": "fg red",
+    "title": "fg cyan underline bold",
+    "fatal": "fg red bold",
+    "highlight": "bg yellow",
+    "info": "fg blue",
+    "success": "fg green",
+}
 
 # Functions to be used for color name operations
 
diff --git a/plumbum/colorlib/styles.py b/plumbum/colorlib/styles.py
index 4e8b276..8278bb2 100644
--- a/plumbum/colorlib/styles.py
+++ b/plumbum/colorlib/styles.py
@@ -346,7 +346,7 @@ class Style(metaclass=ABCMeta):
     end = "\n"
     """The endline character. Override if needed in subclasses."""
 
-    ANSI_REG = re.compile("\033" + r"\[([\d;]+)m")
+    ANSI_REG = re.compile("\033\\[([\\d;]+)m")
     """The regular expression that finds ansi codes in a string."""
 
     @property
@@ -385,8 +385,7 @@ class Style(metaclass=ABCMeta):
 
     @classmethod
     def from_color(cls, color):
-        self = cls(fgcolor=color) if color.fg else cls(bgcolor=color)
-        return self
+        return cls(fgcolor=color) if color.fg else cls(bgcolor=color)
 
     def invert(self):
         """This resets current color(s) and flips the value of all
@@ -575,7 +574,6 @@ class Style(metaclass=ABCMeta):
     def __eq__(self, other):
         """Equality is true only if reset, or if attributes, fg, and bg match."""
         if type(self) == type(other):
-
             if self.isreset:
                 return other.isreset
 
@@ -737,20 +735,19 @@ class HTMLStyle(Style):
     actually can be a handy way to quickly color html text."""
 
     __slots__ = ()
-    attribute_names = dict(
-        bold="b",
-        em="em",
-        italics="i",
-        li="li",
-        underline='span style="text-decoration: underline;"',
-        code="code",
-        ol="ol start=0",
-        strikeout="s",
-    )
+    attribute_names = {
+        "bold": "b",
+        "em": "em",
+        "italics": "i",
+        "li": "li",
+        "underline": 'span style="text-decoration: underline;"',
+        "code": "code",
+        "ol": "ol start=0",
+        "strikeout": "s",
+    }
     end = "<br/>\n"
 
     def __str__(self):
-
         if self.isreset:
             raise ResetNotSupported("HTML does not support global resets!")
 
@@ -764,7 +761,7 @@ class HTMLStyle(Style):
             if self.attributes[attr]:
                 result += "<" + self.attribute_names[attr] + ">"
 
-        for attr in reversed(sorted(self.attributes)):
+        for attr in sorted(self.attributes, reverse=True):
             if not self.attributes[attr]:
                 result += "</" + self.attribute_names[attr].split(" ")[0] + ">"
         if self.fg and self.fg.isreset:
diff --git a/plumbum/commands/base.py b/plumbum/commands/base.py
index 7de93da..52c0f26 100644
--- a/plumbum/commands/base.py
+++ b/plumbum/commands/base.py
@@ -377,11 +377,11 @@ class Pipeline(BaseCommand):
         return self.srccmd._get_encoding() or self.dstcmd._get_encoding()
 
     def formulate(self, level=0, args=()):
-        return (
-            self.srccmd.formulate(level + 1)
-            + ["|"]
-            + self.dstcmd.formulate(level + 1, args)
-        )
+        return [
+            *self.srccmd.formulate(level + 1),
+            "|",
+            *self.dstcmd.formulate(level + 1, args),
+        ]
 
     @property
     def machine(self):
@@ -422,12 +422,10 @@ class Pipeline(BaseCommand):
             # TODO: right now it's impossible to specify different expected
             # return codes for different stages of the pipeline
             try:
-                or_retcode = [0] + list(retcode)
+                or_retcode = [0, *list(retcode)]
             except TypeError:
-                if retcode is None:
-                    or_retcode = None  # no-retcode-verification acts "greedily"
-                else:
-                    or_retcode = [0, retcode]
+                # no-retcode-verification acts "greedily"
+                or_retcode = None if retcode is None else [0, retcode]
             proc.srcproc.verify(or_retcode, timeout, stdout, stderr)
             dstproc_verify(retcode, timeout, stdout, stderr)
 
@@ -454,7 +452,8 @@ class BaseRedirection(BaseCommand):
         return f"{self.__class__.__name__}({self.cmd!r}, {self.file!r})"
 
     def formulate(self, level=0, args=()):
-        return self.cmd.formulate(level + 1, args) + [
+        return [
+            *self.cmd.formulate(level + 1, args),
             self.SYM,
             shquote(getattr(self.file, "name", self.file)),
         ]
diff --git a/plumbum/commands/modifiers.py b/plumbum/commands/modifiers.py
index 2082291..98b3749 100644
--- a/plumbum/commands/modifiers.py
+++ b/plumbum/commands/modifiers.py
@@ -329,7 +329,7 @@ class _NOHUP(ExecutionModifier):
     from the current process, returning a
     standard popen object. It will keep running even if you close the current process.
     In order to slightly mimic shell syntax, it applies
-    when you right-and it with a command. If you wish to use a diffent working directory
+    when you right-and it with a command. If you wish to use a different working directory
     or different stdout, stderr, you can use named arguments. The default is ``NOHUP(
     cwd=local.cwd, stdout='nohup.out', stderr=None)``. If stderr is None, stderr will be
     sent to stdout. Use ``os.devnull`` for null output. Will respect redirected output.
@@ -390,7 +390,7 @@ class LogPipe:
             level = self.levels[typ]
             for line in lines.splitlines():
                 if self.prefix:
-                    line = f"{self.prefix}: {line}"
+                    line = f"{self.prefix}: {line}"  # noqa: PLW2901
                 self.log(level, line)
         return popen.returncode
 
diff --git a/plumbum/commands/processes.py b/plumbum/commands/processes.py
index 2733983..2ccb498 100644
--- a/plumbum/commands/processes.py
+++ b/plumbum/commands/processes.py
@@ -99,10 +99,7 @@ def _iter_lines_win32(proc, decode, linesize, line_timeout=None):
             break
 
 
-if IS_WIN32:
-    _iter_lines = _iter_lines_win32
-else:
-    _iter_lines = _iter_lines_posix
+_iter_lines = _iter_lines_win32 if IS_WIN32 else _iter_lines_posix
 
 
 # ===================================================================================================
@@ -115,9 +112,7 @@ class ProcessExecutionError(OSError):
     well as the command line used to create the process (``argv``)
     """
 
-    # pylint: disable-next=super-init-not-called
     def __init__(self, argv, retcode, stdout, stderr, message=None, *, host=None):
-
         # we can't use 'super' here since OSError only keeps the first 2 args,
         # which leads to failuring in loading this object from a pickle.dumps.
         # pylint: disable-next=non-parent-init-called
@@ -256,7 +251,7 @@ def _register_proc_timeout(proc, timeout):
 
 
 def _shutdown_bg_threads():
-    global _shutting_down  # pylint: disable=global-statement
+    global _shutting_down  # noqa: PLW0603
     _shutting_down = True
     # Make sure this still exists (don't throw error in atexit!)
     # TODO: not sure why this would be "falsey", though
@@ -370,7 +365,6 @@ def iter_lines(
 
     buffers = [[], []]
     for t, line in _iter_lines(proc, decode, linesize, line_timeout):
-
         # verify that the proc hasn't timed out yet
         proc.verify(timeout=timeout, retcode=None, stdout=None, stderr=None)
 
diff --git a/plumbum/fs/atomic.py b/plumbum/fs/atomic.py
index 5209648..813e3a3 100644
--- a/plumbum/fs/atomic.py
+++ b/plumbum/fs/atomic.py
@@ -127,15 +127,14 @@ class AtomicFile:
         if self._owned_by == threading.get_ident():
             yield
             return
-        with self._thdlock:
-            with locked_file(self._fileobj.fileno(), blocking):
-                if not self.path.exists() and not self._ignore_deletion:
-                    raise ValueError("Atomic file removed from filesystem")
-                self._owned_by = threading.get_ident()
-                try:
-                    yield
-                finally:
-                    self._owned_by = None
+        with self._thdlock, locked_file(self._fileobj.fileno(), blocking):
+            if not self.path.exists() and not self._ignore_deletion:
+                raise ValueError("Atomic file removed from filesystem")
+            self._owned_by = threading.get_ident()
+            try:
+                yield
+            finally:
+                self._owned_by = None
 
     def delete(self):
         """
@@ -233,10 +232,7 @@ class AtomicCounterFile:
         """
         with self.atomicfile.locked():
             curr = self.atomicfile.read_atomic().decode("utf8")
-            if not curr:
-                curr = self.initial
-            else:
-                curr = int(curr)
+            curr = self.initial if not curr else int(curr)
             self.atomicfile.write_atomic(str(curr + 1).encode("utf8"))
             return curr
 
@@ -301,9 +297,8 @@ class PidFile:
                 f"PID file {self.atomicfile.path!r} taken by process {pid}",
                 pid,
             ) from None
-        else:
-            self.atomicfile.write_atomic(str(os.getpid()).encode("utf8"))
-            atexit.register(self.release)
+        self.atomicfile.write_atomic(str(os.getpid()).encode("utf8"))
+        atexit.register(self.release)
 
     def release(self):
         """
diff --git a/plumbum/machines/_windows.py b/plumbum/machines/_windows.py
index 0118c73..b289bcf 100644
--- a/plumbum/machines/_windows.py
+++ b/plumbum/machines/_windows.py
@@ -17,8 +17,7 @@ def get_pe_subsystem(filename):
         if f.read(4) != b"PE\x00\x00":
             return None
         f.seek(FILE_HEADER_SIZE + SUBSYSTEM_OFFSET, 1)
-        subsystem = struct.unpack("H", f.read(2))[0]
-        return subsystem
+        return struct.unpack("H", f.read(2))[0]
 
 
 # print(get_pe_subsystem("c:\\windows\\notepad.exe")) == 2
diff --git a/plumbum/machines/base.py b/plumbum/machines/base.py
index 754852b..aa94f17 100644
--- a/plumbum/machines/base.py
+++ b/plumbum/machines/base.py
@@ -63,8 +63,7 @@ class BaseMachine:
             self[cmd]
         except CommandNotFound:
             return False
-        else:
-            return True
+        return True
 
     @property
     def encoding(self):
diff --git a/plumbum/machines/env.py b/plumbum/machines/env.py
index a430632..60df5a1 100644
--- a/plumbum/machines/env.py
+++ b/plumbum/machines/env.py
@@ -183,5 +183,4 @@ class BaseEnv:
             import pwd
         except ImportError:
             return None
-        else:
-            return pwd.getpwuid(os.getuid())[0]  # @UndefinedVariable
+        return pwd.getpwuid(os.getuid())[0]  # @UndefinedVariable
diff --git a/plumbum/machines/local.py b/plumbum/machines/local.py
index 5f1630c..9009863 100644
--- a/plumbum/machines/local.py
+++ b/plumbum/machines/local.py
@@ -149,9 +149,10 @@ class LocalMachine(BaseMachine):
         self._as_user_stack = []
 
     if IS_WIN32:
-        _EXTENSIONS = [""] + env.get("PATHEXT", ":.exe:.bat").lower().split(
-            os.path.pathsep
-        )
+        _EXTENSIONS = [
+            "",
+            *env.get("PATHEXT", ":.exe:.bat").lower().split(os.path.pathsep),
+        ]
 
         @classmethod
         def _which(cls, progname):
@@ -217,8 +218,7 @@ class LocalMachine(BaseMachine):
             self[cmd]
         except CommandNotFound:
             return False
-        else:
-            return True
+        return True
 
     def __getitem__(self, cmd):
         """Returns a `Command` object representing the given program. ``cmd`` can be a string or
@@ -233,6 +233,8 @@ class LocalMachine(BaseMachine):
             return LocalCommand(cmd)
 
         if not isinstance(cmd, RemotePath):
+            # handle "path-like" (pathlib.Path) objects
+            cmd = os.fspath(cmd)
             if "/" in cmd or "\\" in cmd:
                 # assume path
                 return LocalCommand(local.path(cmd))
@@ -424,12 +426,12 @@ class LocalMachine(BaseMachine):
         else:
             if username is None:
                 self._as_user_stack.append(
-                    lambda argv: (["sudo"] + list(argv), self.which("sudo"))
+                    lambda argv: (["sudo", *list(argv)], self.which("sudo"))
                 )
             else:
                 self._as_user_stack.append(
                     lambda argv: (
-                        ["sudo", "-u", username] + list(argv),
+                        ["sudo", "-u", username, *list(argv)],
                         self.which("sudo"),
                     )
                 )
diff --git a/plumbum/machines/paramiko_machine.py b/plumbum/machines/paramiko_machine.py
index 6b16d17..04ea8f1 100644
--- a/plumbum/machines/paramiko_machine.py
+++ b/plumbum/machines/paramiko_machine.py
@@ -54,9 +54,8 @@ class ParamikoPopen(PopenAddons):
         self.stderr_file = stderr_file
 
     def poll(self):
-        if self.returncode is None:
-            if self.channel.exit_status_ready():
-                return self.wait()
+        if self.returncode is None and self.channel.exit_status_ready():
+            return self.wait()
         return self.returncode
 
     def wait(self):
@@ -286,7 +285,13 @@ class ParamikoMachine(BaseRemoteMachine):
         return self._sftp
 
     def session(
-        self, isatty=False, term="vt100", width=80, height=24, *, new_session=False
+        self,
+        isatty=False,
+        term="vt100",
+        width=80,
+        height=24,
+        *,
+        new_session=False,  # noqa: ARG002
     ):
         # new_session is ignored for ParamikoMachine
         trans = self._client.get_transport()
@@ -308,7 +313,7 @@ class ParamikoMachine(BaseRemoteMachine):
         stdin=None,
         stdout=None,
         stderr=None,
-        new_session=False,  # pylint: disable=unused-argument
+        new_session=False,  # noqa: ARG002
         env=None,
         cwd=None,
     ):
@@ -479,11 +484,10 @@ class SocketCompatibleChannel:
 ###################################################################################################
 def _iter_lines(
     proc,
-    decode,  # pylint: disable=unused-argument
+    decode,  # noqa: ARG001
     linesize,
     line_timeout=None,
 ):
-
     from selectors import EVENT_READ, DefaultSelector
 
     # Python 3.4+ implementation
diff --git a/plumbum/machines/remote.py b/plumbum/machines/remote.py
index 52ab652..377da46 100644
--- a/plumbum/machines/remote.py
+++ b/plumbum/machines/remote.py
@@ -275,7 +275,8 @@ class BaseRemoteMachine(BaseMachine):
 
     def session(self, isatty=False, *, new_session=False):
         """Creates a new :class:`ShellSession <plumbum.session.ShellSession>` object; this invokes the user's
-        shell on the remote machine and executes commands on it over stdin/stdout/stderr"""
+        shell on the remote machine and executes commands on it over stdin/stdout/stderr
+        """
         raise NotImplementedError()
 
     def download(self, src, dst):
@@ -379,7 +380,7 @@ class BaseRemoteMachine(BaseMachine):
             return None
         statres = out.strip().split(",")
         text_mode = statres.pop(0).lower()
-        res = StatRes((int(statres[0], 16),) + tuple(int(sr) for sr in statres[1:]))
+        res = StatRes((int(statres[0], 16), *tuple(int(sr) for sr in statres[1:])))
         res.text_mode = text_mode
         return res
 
@@ -395,7 +396,7 @@ class BaseRemoteMachine(BaseMachine):
     def _path_mkdir(
         self,
         fn,
-        mode=None,  # pylint: disable=unused-argument
+        mode=None,  # noqa: ARG002
         minus_p=True,
     ):
         p_str = "-p " if minus_p else ""
@@ -424,7 +425,7 @@ class BaseRemoteMachine(BaseMachine):
     def _path_read(self, fn):
         data = self["cat"](fn)
         if self.custom_encoding and isinstance(data, str):
-            data = data.encode(self.custom_encoding)
+            return data.encode(self.custom_encoding)
         return data
 
     def _path_write(self, fn, data):
diff --git a/plumbum/machines/session.py b/plumbum/machines/session.py
index 7ffb042..fef838e 100644
--- a/plumbum/machines/session.py
+++ b/plumbum/machines/session.py
@@ -49,7 +49,7 @@ class MarkedPipe:
         self.marker = bytes(self.marker, "ascii")
 
     def close(self):
-        """'Closes' the marked pipe; following calls to ``readline`` will return """ ""
+        """'Closes' the marked pipe; following calls to ``readline`` will return "" """
         # consume everything
         while self.readline():
             pass
@@ -65,7 +65,7 @@ class MarkedPipe:
             raise EOFError()
         if line.strip() == self.marker:
             self.pipe = None
-            line = b""
+            return b""
         return line
 
 
@@ -277,10 +277,7 @@ class ShellSession:
         if self._current and not self._current._done:
             raise ShellSessionError("Each shell may start only one process at a time")
 
-        if isinstance(cmd, BaseCommand):
-            full_cmd = cmd.formulate(1)
-        else:
-            full_cmd = cmd
+        full_cmd = cmd.formulate(1) if isinstance(cmd, BaseCommand) else cmd
         marker = f"--.END{time.time() * random.random()}.--"
         if full_cmd.strip():
             full_cmd += " ; "
diff --git a/plumbum/machines/ssh_machine.py b/plumbum/machines/ssh_machine.py
index 79bad6b..82eb29c 100644
--- a/plumbum/machines/ssh_machine.py
+++ b/plumbum/machines/ssh_machine.py
@@ -127,7 +127,6 @@ class SshMachine(BaseRemoteMachine):
         connect_timeout=10,
         new_session=False,
     ):
-
         if ssh_command is None:
             if password is not None:
                 ssh_command = local["sshpass"]["-p", password, "ssh"]
@@ -196,7 +195,11 @@ class SshMachine(BaseRemoteMachine):
         allowing the command to run "detached" from its controlling TTY or parent.
         Does not return anything. Depreciated (use command.nohup or daemonic_popen).
         """
-        warnings.warn("Use .nohup on the command or use daemonic_popen)", FutureWarning)
+        warnings.warn(
+            "Use .nohup on the command or use daemonic_popen)",
+            FutureWarning,
+            stacklevel=2,
+        )
         self.daemonic_popen(command, cwd=".", stdout=None, stderr=None, append=False)
 
     def daemonic_popen(self, command, cwd=".", stdout=None, stderr=None, append=True):
@@ -213,10 +216,7 @@ class SshMachine(BaseRemoteMachine):
         if stderr is None:
             stderr = "&1"
 
-        if str(cwd) == ".":
-            args = []
-        else:
-            args = ["cd", str(cwd), "&&"]
+        args = [] if str(cwd) == "." else ["cd", str(cwd), "&&"]
         args.append("nohup")
         args.extend(command.formulate())
         args.extend(
@@ -255,7 +255,7 @@ class SshMachine(BaseRemoteMachine):
         dport,
         lhost="localhost",
         dhost="localhost",
-        connect_timeout=5,  # pylint: disable=unused-argument
+        connect_timeout=5,  # noqa: ARG002
         reverse=False,
     ):
         r"""Creates an SSH tunnel from the TCP port (``lport``) of the local machine
@@ -332,11 +332,12 @@ class SshMachine(BaseRemoteMachine):
             reverse,
         )
 
-    def _translate_drive_letter(self, path):  # pylint: disable=no-self-use
+    @staticmethod
+    def _translate_drive_letter(path):
         # replace c:\some\path with /c/some/path
         path = str(path)
         if ":" in path:
-            path = "/" + path.replace(":", "").replace("\\", "/")
+            return "/" + path.replace(":", "").replace("\\", "/")
         return path
 
     def download(self, src, dst):
@@ -396,7 +397,7 @@ class PuttyMachine(SshMachine):
             user = local.env.user
         if port is not None:
             ssh_opts.extend(["-P", str(port)])
-            scp_opts = list(scp_opts) + ["-P", str(port)]
+            scp_opts = [*list(scp_opts), "-P", str(port)]
             port = None
         SshMachine.__init__(
             self,
diff --git a/plumbum/path/base.py b/plumbum/path/base.py
index 10e1862..1ae6798 100644
--- a/plumbum/path/base.py
+++ b/plumbum/path/base.py
@@ -1,6 +1,8 @@
+import io
 import itertools
 import operator
 import os
+import typing
 import warnings
 from abc import ABC, abstractmethod
 from functools import reduce
@@ -20,6 +22,9 @@ class FSUser(int):
         return self
 
 
+_PathImpl = typing.TypeVar("_PathImpl", bound="Path")
+
+
 class Path(str, ABC):
     """An abstraction over file system paths. This class is abstract, and the two implementations
     are :class:`LocalPath <plumbum.machines.local.LocalPath>` and
@@ -31,7 +36,7 @@ class Path(str, ABC):
     def __repr__(self):
         return f"<{self.__class__.__name__} {self}>"
 
-    def __truediv__(self, other):
+    def __truediv__(self: _PathImpl, other: typing.Any) -> _PathImpl:
         """Joins two paths"""
         return self.join(other)
 
@@ -48,7 +53,7 @@ class Path(str, ABC):
         """Iterate over the files in this directory"""
         return iter(self.list())
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         if isinstance(other, Path):
             return self._get_info() == other._get_info()
         if isinstance(other, str):
@@ -92,7 +97,7 @@ class Path(str, ABC):
             return (self / item).exists()
 
     @abstractmethod
-    def _form(self, *parts):
+    def _form(self: _PathImpl, *parts: typing.Any) -> _PathImpl:
         pass
 
     def up(self, count=1):
@@ -101,8 +106,8 @@ class Path(str, ABC):
 
     def walk(
         self,
-        filter=lambda p: True,  # pylint: disable=redefined-builtin
-        dir_filter=lambda p: True,
+        filter=lambda _: True,  # pylint: disable=redefined-builtin
+        dir_filter=lambda _: True,
     ):
         """traverse all (recursive) sub-elements under this directory, that match the given filter.
         By default, the filter accepts everything; you can provide a custom filter function that
@@ -121,120 +126,122 @@ class Path(str, ABC):
 
     @property
     @abstractmethod
-    def name(self):
+    def name(self) -> str:
         """The basename component of this path"""
 
     @property
     def basename(self):
         """Included for compatibility with older Plumbum code"""
-        warnings.warn("Use .name instead", FutureWarning)
+        warnings.warn("Use .name instead", FutureWarning, stacklevel=2)
         return self.name
 
     @property
     @abstractmethod
-    def stem(self):
+    def stem(self) -> str:
         """The name without an extension, or the last component of the path"""
 
     @property
     @abstractmethod
-    def dirname(self):
+    def dirname(self: _PathImpl) -> _PathImpl:
         """The dirname component of this path"""
 
     @property
     @abstractmethod
-    def root(self):
+    def root(self) -> str:
         """The root of the file tree (`/` on Unix)"""
 
     @property
     @abstractmethod
-    def drive(self):
+    def drive(self) -> str:
         """The drive letter (on Windows)"""
 
     @property
     @abstractmethod
-    def suffix(self):
+    def suffix(self) -> str:
         """The suffix of this file"""
 
     @property
     @abstractmethod
-    def suffixes(self):
+    def suffixes(self) -> typing.List[str]:
         """This is a list of all suffixes"""
 
     @property
     @abstractmethod
-    def uid(self):
+    def uid(self) -> FSUser:
         """The user that owns this path. The returned value is a :class:`FSUser <plumbum.path.FSUser>`
         object which behaves like an ``int`` (as expected from ``uid``), but it also has a ``.name``
         attribute that holds the string-name of the user"""
 
     @property
     @abstractmethod
-    def gid(self):
+    def gid(self) -> FSUser:
         """The group that owns this path. The returned value is a :class:`FSUser <plumbum.path.FSUser>`
         object which behaves like an ``int`` (as expected from ``gid``), but it also has a ``.name``
         attribute that holds the string-name of the group"""
 
     @abstractmethod
-    def as_uri(self, scheme=None):
+    def as_uri(self, scheme: typing.Optional[str] = None) -> str:
         """Returns a universal resource identifier. Use ``scheme`` to force a scheme."""
 
     @abstractmethod
-    def _get_info(self):
+    def _get_info(self) -> typing.Any:
         pass
 
     @abstractmethod
-    def join(self, *parts):
+    def join(self: _PathImpl, *parts: typing.Any) -> _PathImpl:
         """Joins this path with any number of paths"""
 
     @abstractmethod
-    def list(self):
+    def list(self: _PathImpl) -> typing.List[_PathImpl]:
         """Returns the files in this directory"""
 
     @abstractmethod
-    def iterdir(self):
+    def iterdir(self: _PathImpl) -> typing.Iterable[_PathImpl]:
         """Returns an iterator over the directory. Might be slightly faster on Python 3.5 than .list()"""
 
     @abstractmethod
-    def is_dir(self):
+    def is_dir(self) -> bool:
         """Returns ``True`` if this path is a directory, ``False`` otherwise"""
 
     def isdir(self):
         """Included for compatibility with older Plumbum code"""
-        warnings.warn("Use .is_dir() instead", FutureWarning)
+        warnings.warn("Use .is_dir() instead", FutureWarning, stacklevel=2)
         return self.is_dir()
 
     @abstractmethod
-    def is_file(self):
+    def is_file(self) -> bool:
         """Returns ``True`` if this path is a regular file, ``False`` otherwise"""
 
-    def isfile(self):
+    def isfile(self) -> bool:
         """Included for compatibility with older Plumbum code"""
-        warnings.warn("Use .is_file() instead", FutureWarning)
+        warnings.warn("Use .is_file() instead", FutureWarning, stacklevel=2)
         return self.is_file()
 
     def islink(self):
         """Included for compatibility with older Plumbum code"""
-        warnings.warn("Use is_symlink instead", FutureWarning)
+        warnings.warn("Use is_symlink instead", FutureWarning, stacklevel=2)
         return self.is_symlink()
 
     @abstractmethod
-    def is_symlink(self):
+    def is_symlink(self) -> bool:
         """Returns ``True`` if this path is a symbolic link, ``False`` otherwise"""
 
     @abstractmethod
-    def exists(self):
+    def exists(self) -> bool:
         """Returns ``True`` if this path exists, ``False`` otherwise"""
 
     @abstractmethod
-    def stat(self):
+    def stat(self) -> os.stat_result:
         """Returns the os.stats for a file"""
 
     @abstractmethod
-    def with_name(self, name):
+    def with_name(self: _PathImpl, name: typing.Any) -> _PathImpl:
         """Returns a path with the name replaced"""
 
     @abstractmethod
-    def with_suffix(self, suffix, depth=1):
+    def with_suffix(
+        self: _PathImpl, suffix: str, depth: typing.Optional[int] = 1
+    ) -> _PathImpl:
         """Returns a path with the suffix replaced. Up to last ``depth`` suffixes will be
         replaced. None will replace all suffixes. If there are less than ``depth`` suffixes,
         this will replace all suffixes. ``.tar.gz`` is an example where ``depth=2`` or
@@ -246,7 +253,9 @@ class Path(str, ABC):
         return self if len(self.suffixes) > 0 else self.with_suffix(suffix)
 
     @abstractmethod
-    def glob(self, pattern):
+    def glob(
+        self: _PathImpl, pattern: typing.Union[str, typing.Iterable[str]]
+    ) -> typing.List[_PathImpl]:
         """Returns a (possibly empty) list of paths that matched the glob-pattern under this path"""
 
     @abstractmethod
@@ -289,16 +298,18 @@ class Path(str, ABC):
         """
 
     @abstractmethod
-    def open(self, mode="r", *, encoding=None):
+    def open(
+        self, mode: str = "r", *, encoding: typing.Optional[str] = None
+    ) -> io.IOBase:
         """opens this path as a file"""
 
     @abstractmethod
-    def read(self, encoding=None):
+    def read(self, encoding: typing.Optional[str] = None) -> str:
         """returns the contents of this file as a ``str``. By default the data is read
         as text, but you can specify the encoding, e.g., ``'latin1'`` or ``'utf8'``"""
 
     @abstractmethod
-    def write(self, data, encoding=None):
+    def write(self, data: typing.AnyStr, encoding: typing.Optional[str] = None) -> None:
         """writes the given data to this file. By default the data is written as-is
         (either text or binary), but you can specify the encoding, e.g., ``'latin1'``
         or ``'utf8'``"""
@@ -331,12 +342,12 @@ class Path(str, ABC):
             flags = FLAGS
 
         if isinstance(mode, str):
-            mode = reduce(operator.or_, [flags[m] for m in mode.lower()], 0)
+            return reduce(operator.or_, [flags[m] for m in mode.lower()], 0)
 
         return mode
 
     @abstractmethod
-    def access(self, mode=0):
+    def access(self, mode: typing.Union[int, str] = 0) -> bool:
         """Test file existence or permission bits
 
         :param mode: a bitwise-or of access bits, or a string-representation thereof:
@@ -376,7 +387,7 @@ class Path(str, ABC):
     @property
     def parts(self):
         """Splits the directory into parts, including the base directory, returns a tuple"""
-        return tuple([self.drive + self.root] + self.split())
+        return (self.drive + self.root, *self.split())
 
     def relative_to(self, source):
         """Computes the "relative path" require to get from ``source`` to ``self``. They satisfy the invariant
@@ -413,7 +424,7 @@ class Path(str, ABC):
             results.extend(fn(single_pattern))
         return sorted(list(set(results)))
 
-    def resolve(self, strict=False):  # pylint:disable=unused-argument
+    def resolve(self, strict=False):  # noqa: ARG002
         """Added to allow pathlib like syntax. Does nothing since
         Plumbum paths are always absolute. Does not (currently) resolve
         symlinks."""
diff --git a/plumbum/path/local.py b/plumbum/path/local.py
index fe0e507..068aa0e 100644
--- a/plumbum/path/local.py
+++ b/plumbum/path/local.py
@@ -53,10 +53,9 @@ class LocalPath(Path):
             raise TypeError("At least one path part is required (none given)")
         if any(isinstance(path, RemotePath) for path in parts):
             raise TypeError(f"LocalPath cannot be constructed from {parts!r}")
-        self = super().__new__(
+        return super().__new__(
             cls, os.path.normpath(os.path.join(*(str(p) for p in parts)))
         )
-        return self
 
     @property
     def _path(self):
@@ -216,17 +215,14 @@ class LocalPath(Path):
         with self.open(mode) as f:
             data = f.read()
             if encoding:
-                data = data.decode(encoding)
+                return data.decode(encoding)
             return data
 
     def write(self, data, encoding=None, mode=None):
         if encoding:
             data = data.encode(encoding)
         if mode is None:
-            if isinstance(data, str):
-                mode = "w"
-            else:
-                mode = "wb"
+            mode = "w" if isinstance(data, str) else "wb"
         with self.open(mode) as f:
             f.write(data)
 
diff --git a/plumbum/path/remote.py b/plumbum/path/remote.py
index b17d7e6..19c5c4d 100644
--- a/plumbum/path/remote.py
+++ b/plumbum/path/remote.py
@@ -47,7 +47,7 @@ class RemotePath(Path):
                 if hasattr(remote, "_cwd")
                 else remote._session.run("pwd")[1].strip()
             )
-            parts = (cwd,) + parts
+            parts = (cwd, *parts)
 
         for p in parts:
             if windows:
@@ -237,7 +237,7 @@ class RemotePath(Path):
     def read(self, encoding=None):
         data = self.remote._path_read(self)
         if encoding:
-            data = data.decode(encoding)
+            return data.decode(encoding)
         return data
 
     def write(self, data, encoding=None):
@@ -323,8 +323,7 @@ class RemoteWorkdir(RemotePath):
     """Remote working directory manipulator"""
 
     def __new__(cls, remote):
-        self = super().__new__(cls, remote, remote._session.run("pwd")[1].strip())
-        return self
+        return super().__new__(cls, remote, remote._session.run("pwd")[1].strip())
 
     def __hash__(self):
         raise TypeError("unhashable type")
diff --git a/plumbum/typed_env.py b/plumbum/typed_env.py
index fe88aae..c48e48a 100644
--- a/plumbum/typed_env.py
+++ b/plumbum/typed_env.py
@@ -142,8 +142,7 @@ class TypedEnv(MutableMapping):
             self._raw_get(key)
         except EnvironmentVariableError:
             return False
-        else:
-            return True
+        return True
 
     def __getattr__(self, name):
         # if we're here then there was no descriptor defined
diff --git a/plumbum/version.py b/plumbum/version.py
index 2840f02..b479b9f 100644
--- a/plumbum/version.py
+++ b/plumbum/version.py
@@ -1,5 +1,4 @@
-# coding: utf-8
 # file generated by setuptools_scm
 # don't change, don't track in version control
-__version__ = version = '1.8.0'
-__version_tuple__ = version_tuple = (1, 8, 0)
+__version__ = version = '1.8.2'
+__version_tuple__ = version_tuple = (1, 8, 2)
diff --git a/pyproject.toml b/pyproject.toml
index 8a7ee77..aeaa098 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,13 +1,81 @@
 [build-system]
 requires = [
-    "setuptools>=42",
-    "wheel",
-    "setuptools_scm[toml]>=3.4.3"
+    "hatchling",
+    "hatch-vcs",
 ]
-build-backend = "setuptools.build_meta"
+build-backend = "hatchling.build"
 
-[tool.setuptools_scm]
-write_to = "plumbum/version.py"
+
+[project]
+name = "plumbum"
+description = "Plumbum: shell combinators library"
+readme = "README.rst"
+authors = [{ name="Tomer Filiba", email="tomerfiliba@gmail.com" }]
+license = { file="LICENSE" }
+requires-python = ">=3.6"
+dynamic = ["version"]
+dependencies = [
+    "pywin32; platform_system=='Windows' and platform_python_implementation!='PyPy'",
+]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.6",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development :: Build Tools",
+    "Topic :: System :: Systems Administration",
+]
+keywords = [
+    "path",
+    "local",
+    "remote",
+    "ssh",
+    "shell",
+    "pipe",
+    "popen",
+    "process",
+    "execution",
+    "color",
+    "cli",
+]
+
+[project.urls]
+Homepage = "https://github.com/tomerfiliba/plumbum"
+Documentation = "https://plumbum.readthedocs.io/"
+"Bug Tracker" = "https://github.com/tomerfiliba/plumbum/issues"
+Changelog = "https://plumbum.readthedocs.io/en/latest/changelog.html"
+Cheatsheet = "https://plumbum.readthedocs.io/en/latest/quickref.html"
+
+
+[project.optional-dependencies]
+dev = [
+    "paramiko",
+    "psutil",
+    "pytest>=6.0",
+    "pytest-cov",
+    "pytest-mock",
+    "pytest-timeout",
+]
+docs = [
+    "sphinx>=4.0.0",
+    "sphinx-rtd-theme>=1.0.0",
+]
+ssh = [
+    "paramiko",
+]
+
+[tool.hatch]
+version.source = "vcs"
+build.hooks.vcs.version-file = "plumbum/version.py"
 
 
 [tool.mypy]
@@ -30,7 +98,6 @@ warn_return_any = false
 no_implicit_reexport = true
 strict_equality = true
 
-
 [[tool.mypy.overrides]]
 module = ["IPython.*", "pywintypes.*", "win32con.*", "win32file.*", "PIL.*", "plumbum.cmd.*", "ipywidgets.*", "traitlets.*", "plumbum.version"]
 ignore_missing_imports = true
@@ -51,19 +118,6 @@ optional_tests = """
   sudo: requires sudo access to run
 """
 
-[tool.isort]
-profile = "black"
-
-[tool.check-manifest]
-ignore = [
-  ".*",
-  "docs/**",
-  "examples/*",
-  "experiments/*",
-  "conda.recipe/*",
-  "CONTRIBUTING.rst",
-]
-
 
 [tool.pylint]
 master.py-version = "3.6"
@@ -107,4 +161,48 @@ messages_control.disable = [
   "unnecessary-lambda-assignment", # TODO: 4 instances
   "unused-import", # identical to flake8 but has typing false positives
   "eval-used",  # Needed for Python <3.10 annotations
+  "unused-argument", # Covered by ruff
+  "global-statement", # Covered by ruff
+  "pointless-statement", # Covered by ruff
 ]
+
+[tool.ruff]
+select = [
+  "E", "F", "W", # flake8
+  "B", "B904",   # flake8-bugbear
+  "I",           # isort
+  "ARG",         # flake8-unused-arguments
+  "C4",          # flake8-comprehensions
+  "ICN",         # flake8-import-conventions
+  "ISC",         # flake8-implicit-str-concat
+  "PGH",         # pygrep-hooks
+  "PIE",         # flake8-pie
+  "PL",          # pylint
+  "PT",          # flake8-pytest-style
+  "RET",         # flake8-return
+  "RUF",         # Ruff-specific
+  "SIM",         # flake8-simplify
+  "T20",         # flake8-print
+  "UP",          # pyupgrade
+  "YTT",         # flake8-2020
+
+]
+extend-ignore = [
+  "E501",
+  "PLR",
+  "PT004",
+  "PT011",  # TODO: add match parameter
+  "PLC1901", # Simplify == "", might be risky
+]
+target-version = "py37"
+exclude = ["docs/conf.py"]
+mccabe.max-complexity = 50
+flake8-unused-arguments.ignore-variadic-names = true
+
+[tool.ruff.per-file-ignores]
+"examples/*" = ["T20"]
+"experiments/*" = ["T20"]
+"tests/*" = ["T20"]
+"plumbum/cli/application.py" = ["T20"]
+"plumbum/commands/base.py" = ["SIM115"]
+"plumbum/commands/daemons.py" = ["SIM115"]
diff --git a/setup.cfg b/setup.cfg
index 999ac7e..e1bc49d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,107 +1,31 @@
-[metadata]
-name = plumbum
-description = Plumbum: shell combinators library
-long_description = file: README.rst
-long_description_content_type = text/x-rst
-url = https://plumbum.readthedocs.io
-author = Tomer Filiba
-author_email = tomerfiliba@gmail.com
-license = MIT
-license_file = LICENSE
-platforms = POSIX, Windows
-classifiers = 
-	Development Status :: 5 - Production/Stable
-	License :: OSI Approved :: MIT License
-	Operating System :: Microsoft :: Windows
-	Operating System :: POSIX
-	Programming Language :: Python :: 3
-	Programming Language :: Python :: 3 :: Only
-	Programming Language :: Python :: 3.6
-	Programming Language :: Python :: 3.7
-	Programming Language :: Python :: 3.8
-	Programming Language :: Python :: 3.9
-	Programming Language :: Python :: 3.10
-	Programming Language :: Python :: 3.11
-	Topic :: Software Development :: Build Tools
-	Topic :: System :: Systems Administration
-keywords = 
-	path,
-	local,
-	remote,
-	ssh,
-	shell,
-	pipe,
-	popen,
-	process,
-	execution,
-	color,
-	cli
-project_urls = 
-	Bug Tracker = https://github.com/tomerfiliba/plumbum/issues
-	Changelog = https://plumbum.readthedocs.io/en/latest/changelog.html
-	Source = https://github.com/tomerfiliba/plumbum
-provides = plumbum
-
-[options]
-packages = find:
-install_requires = 
-	pywin32;platform_system=='Windows' and platform_python_implementation!="PyPy"
-python_requires = >=3.6
-
-[options.packages.find]
-exclude = 
-	tests
-
-[options.extras_require]
-dev = 
-	paramiko
-	psutil
-	pytest>=6.0
-	pytest-cov
-	pytest-mock
-	pytest-timeout
-docs = 
-	Sphinx>=4.0.0
-	sphinx-rtd-theme>=1.0.0
-ssh = 
-	paramiko
-
-[options.package_data]
-plumbum.cli = i18n/*/LC_MESSAGES/*.mo
-
 [coverage:run]
 branch = True
 relative_files = True
-source_pkgs = 
-	plumbum
-omit = 
-	*ipython*.py
-	*__main__.py
-	*_windows.py
+source_pkgs =
+    plumbum
+omit =
+    *ipython*.py
+    *__main__.py
+    *_windows.py
 
 [coverage:report]
-exclude_lines = 
-	pragma: no cover
-	def __repr__
-	raise AssertionError
-	raise NotImplementedError
-	if __name__ == .__main__.:
+exclude_lines =
+    pragma: no cover
+    def __repr__
+    raise AssertionError
+    raise NotImplementedError
+    if __name__ == .__main__.:
 
 [flake8]
 max-complexity = 50
-extend-ignore = E203, E501, B950, T202
-extend-select = B9
-per-file-ignores = 
-	tests/*: T
-	examples/*: T
-	experiments/*: T
-	plumbum/cli/application.py: T
+extend-ignore = E203, E501, T202
+extend-select = B902, B903, B904
+per-file-ignores =
+    tests/*: T
+    examples/*: T
+    experiments/*: T
+    plumbum/cli/application.py: T
 
 [codespell]
 ignore-words-list = ans,switchs,hart,ot,twoo,fo
 skip = *.po
-
-[egg_info]
-tag_build = 
-tag_date = 0
-
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 229b2eb..0000000
--- a/setup.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-
-from setuptools import setup
-
-setup()
diff --git a/tests/conftest.py b/tests/conftest.py
index cd3a7bc..355dfc4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -87,7 +87,7 @@ def pytest_configure(config):
             ot_run = list(re.split(r"[,\s]+", ot_run))
     ot_run = set(ot_run)
 
-    _logger.info("optional tests to run:", ot_run)
+    _logger.info("optional tests to run: %s", ot_run)
     if ot_run:
         unknown_tests = ot_run - ot_markers
         if unknown_tests:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c2512a4..a36284e 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -72,8 +72,7 @@ class GeetCommit(cli.Application):
     def main(self):
         if self.parent.debug:
             return "committing in debug"
-        else:
-            return "committing"
+        return "committing"
 
     def cleanup(self, retcode):
         self.parent.cleanups.append(2)
@@ -237,7 +236,6 @@ class TestCLI:
 
     # Testing #371
     def test_extra_args(self, capsys):
-
         _, rc = PositionalApp.run(["positionalapp"], exit=False)
         assert rc != 0
         stdout, stderr = capsys.readouterr()
@@ -298,7 +296,7 @@ class TestCLI:
             assert "  DEF" in stdout
             assert "   - Item" in stdout
             # List items should not be combined into paragraphs
-            assert "  * Star 2"
+            assert "  * Star 2" in stdout
             # Lines of the same list item should be combined. (The right-hand expression of the 'or' operator
             # below is for when the terminal is too narrow, causing "GHI" to be wrapped to the next line.)
             assert "  GHI" not in stdout or "     GHI" in stdout
@@ -367,7 +365,6 @@ class TestCLI:
         assert inst.eggs == "raw"
 
     def test_mandatory_env_var(self, capsys):
-
         _, rc = SimpleApp.run(["arg"], exit=False)
         assert rc == 2
         stdout, stderr = capsys.readouterr()
diff --git a/tests/test_env.py b/tests/test_env.py
index 6e53993..e76f3f1 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -1,17 +1,17 @@
+import contextlib
+
 from plumbum import local
 from plumbum._testtools import skip_on_windows
 
-try:
+with contextlib.suppress(ModuleNotFoundError):
     from plumbum.cmd import printenv
-except ImportError:
-    pass
 
 
 @skip_on_windows
 class TestEnv:
     def test_change_env(self):
         with local.env(silly=12):
-            assert 12 == local.env["silly"]
+            assert local.env["silly"] == 12
             actual = {x.split("=")[0] for x in printenv().splitlines() if "=" in x}
             localenv = {x[0] for x in local.env}
             print(actual, localenv)
@@ -43,7 +43,7 @@ class TestEnv:
             assert "simple_plum" not in local.env
             local.env["simple_plum"] = "thing"
             assert "simple_plum" in local.env
-            assert "thing" == local.env.pop("simple_plum")
+            assert local.env.pop("simple_plum") == "thing"
             assert "simple_plum" not in local.env
             local.env["simple_plum"] = "thing"
         assert "simple_plum" not in local.env
diff --git a/tests/test_factories.py b/tests/test_factories.py
index f0aff1c..4a1a524 100644
--- a/tests/test_factories.py
+++ b/tests/test_factories.py
@@ -18,7 +18,7 @@ class TestImportColors:
 
 
 class TestANSIColor:
-    def setup_method(self, method):
+    def setup_method(self, method):  # noqa: ARG002
         colors.use_color = True
 
     def testColorSlice(self):
@@ -37,9 +37,9 @@ class TestANSIColor:
         assert colors[1, 30, 77] == colors.rgb(1, 30, 77)
 
     def testColorStrings(self):
-        assert "\033[0m" == colors.reset
-        assert "\033[1m" == colors.bold
-        assert "\033[39m" == colors.fg.reset
+        assert colors.reset == "\033[0m"
+        assert colors.bold == "\033[1m"
+        assert colors.fg.reset == "\033[39m"
 
     def testNegateIsReset(self):
         assert colors.reset == ~colors
@@ -69,10 +69,10 @@ class TestANSIColor:
         assert colors.DeepSkyBlue1 == colors[39]
         assert colors.deepskyblue1 == colors[39]
         assert colors.Deep_Sky_Blue1 == colors[39]
-        assert colors.RED == colors.red
+        assert colors.red == colors.RED
 
         with pytest.raises(AttributeError):
-            colors.Notacolorsatall
+            colors.Notacolorsatall  # noqa: B018
 
     def testMultiColor(self):
         sumcolors = colors.bold & colors.blue
@@ -125,28 +125,28 @@ class TestANSIColor:
         assert newcolors.wrap(string) == string | colors.blue & colors.underline
 
     def testUndoColor(self):
-        assert "\033[39m" == ~colors.fg
-        assert "\033[49m" == ~colors.bg
-        assert "\033[22m" == ~colors.bold
-        assert "\033[22m" == ~colors.dim
+        assert ~colors.fg == "\033[39m"
+        assert ~colors.bg == "\033[49m"
+        assert ~colors.bold == "\033[22m"
+        assert ~colors.dim == "\033[22m"
         for i in range(7):
-            assert "\033[39m" == ~colors(i)
-            assert "\033[49m" == ~colors.bg(i)
-            assert "\033[39m" == ~colors.fg(i)
-            assert "\033[49m" == ~colors.bg(i)
+            assert ~colors(i) == "\033[39m"
+            assert ~colors.bg(i) == "\033[49m"
+            assert ~colors.fg(i) == "\033[39m"
+            assert ~colors.bg(i) == "\033[49m"
         for i in range(256):
-            assert "\033[39m" == ~colors.fg[i]
-            assert "\033[49m" == ~colors.bg[i]
-        assert "\033[0m" == ~colors.reset
+            assert ~colors.fg[i] == "\033[39m"
+            assert ~colors.bg[i] == "\033[49m"
+        assert ~colors.reset == "\033[0m"
         assert colors.do_nothing == ~colors.do_nothing
 
         assert colors.bold.reset == ~colors.bold
 
     def testLackOfColor(self):
         Style.use_color = False
-        assert "" == colors.fg.red
-        assert "" == ~colors.fg
-        assert "" == colors.fg["LightBlue"]
+        assert colors.fg.red == ""
+        assert ~colors.fg == ""
+        assert colors.fg["LightBlue"] == ""
 
     def testFromHex(self):
         with pytest.raises(ColorNotFound):
diff --git a/tests/test_local.py b/tests/test_local.py
index 932ab85..c17981b 100644
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -3,6 +3,7 @@ import pickle
 import signal
 import sys
 import time
+from pathlib import Path
 
 import pytest
 
@@ -33,10 +34,7 @@ SDIR = os.path.dirname(os.path.abspath(__file__))
 
 class TestLocalPopen:
     def test_contextmanager(self):
-        if IS_WIN32:
-            command = ["dir"]
-        else:
-            command = ["ls"]
+        command = ["dir"] if IS_WIN32 else ["ls"]
         with PlumbumLocalPopen(command):
             pass
 
@@ -47,13 +45,14 @@ class TestLocalPath:
     def test_name(self):
         name = self.longpath.name
         assert isinstance(name, str)
-        assert "file.txt" == str(name)
+        assert str(name) == "file.txt"
 
     def test_dirname(self):
         name = self.longpath.dirname
         assert isinstance(name, LocalPath)
-        assert "/some/long/path/to" == str(name).replace("\\", "/").lstrip("C:").lstrip(
-            "D:"
+        assert (
+            str(name).replace("\\", "/").lstrip("C:").lstrip("D:")
+            == "/some/long/path/to"
         )
 
     def test_uri(self):
@@ -62,7 +61,7 @@ class TestLocalPath:
             assert pth.startswith("file:///")
             assert pth.endswith(":/some/long/path/to/file.txt")
         else:
-            assert "file:///some/long/path/to/file.txt" == self.longpath.as_uri()
+            assert self.longpath.as_uri() == "file:///some/long/path/to/file.txt"
 
     def test_pickle(self):
         path1 = local.path(".")
@@ -325,9 +324,11 @@ class TestLocalMachine:
             local["non_exist1N9"]()
 
         with pytest.raises(ImportError):
-            from plumbum.cmd import non_exist1N9
+            from plumbum.cmd import non_exist1N9  # noqa: F401
 
-            assert non_exist1N9
+    def test_pathlib(self):
+        ls_path = Path(local.which("ls"))
+        assert "test_local.py" in local[ls_path]().splitlines()
 
     def test_get(self):
         assert str(local["ls"]) == str(local.get("ls"))
@@ -342,17 +343,16 @@ class TestLocalMachine:
 
     def test_shadowed_by_dir(self):
         real_ls = local["ls"]
-        with local.tempdir() as tdir:
-            with local.cwd(tdir):
-                ls_dir = tdir / "ls"
-                ls_dir.mkdir()
-                fake_ls = local["ls"]
-                assert fake_ls.executable == real_ls.executable
-
-                local.env.path.insert(0, tdir)
-                fake_ls = local["ls"]
-                del local.env.path[0]
-                assert fake_ls.executable == real_ls.executable
+        with local.tempdir() as tdir, local.cwd(tdir):
+            ls_dir = tdir / "ls"
+            ls_dir.mkdir()
+            fake_ls = local["ls"]
+            assert fake_ls.executable == real_ls.executable
+
+            local.env.path.insert(0, tdir)
+            fake_ls = local["ls"]
+            del local.env.path[0]
+            assert fake_ls.executable == real_ls.executable
 
     def test_repr_command(self):
         assert "BG" in repr(BG)
@@ -525,7 +525,7 @@ class TestLocalMachine:
         cat["/dev/urndom"] & FG(1)
         assert "urndom" in capfd.readouterr()[1]
 
-        assert "" == capfd.readouterr()[1]
+        assert capfd.readouterr()[1] == ""
 
         (cat["/dev/urndom"] | head["-c", "10"]) & FG(retcode=1)
         assert "urndom" in capfd.readouterr()[1]
@@ -544,7 +544,7 @@ class TestLocalMachine:
         from plumbum.cmd import bash
 
         cmd = bash["-ce", "for ((i=0;1==1;i++)); do echo $i; sleep .3; done"]
-        with pytest.raises(ProcessTimedOut):
+        with pytest.raises(ProcessTimedOut):  # noqa: PT012
             for i, (out, err) in enumerate(cmd.popen().iter_lines(timeout=1)):
                 assert not err
                 assert out
@@ -556,7 +556,7 @@ class TestLocalMachine:
         from plumbum.cmd import bash
 
         cmd = bash["-ce", "for ((i=0;i<100;i++)); do echo $i; done; false"]
-        with pytest.raises(ProcessExecutionError) as e:
+        with pytest.raises(ProcessExecutionError) as e:  # noqa: PT012
             for _ in cmd.popen().iter_lines(timeout=1, buffer_size=5):
                 pass
         assert e.value.stdout == "\n".join(map(str, range(95, 100))) + "\n"
@@ -571,7 +571,7 @@ class TestLocalMachine:
         ]
         types = {1: "out:", 2: "err:"}
         counts = {1: 0, 2: 0}
-        with pytest.raises(ProcessTimedOut):
+        with pytest.raises(ProcessTimedOut):  # noqa: PT012
             # Order is important on mac
             for typ, line in cmd.popen().iter_lines(timeout=1, mode=BY_TYPE):
                 counts[typ] += 1
@@ -583,7 +583,7 @@ class TestLocalMachine:
     def test_iter_lines_error(self):
         from plumbum.cmd import ls
 
-        with pytest.raises(ProcessExecutionError) as err:
+        with pytest.raises(ProcessExecutionError) as err:  # noqa: PT012
             for i, _lines in enumerate(ls["--bla"].popen()):  # noqa: B007
                 pass
             assert i == 1
@@ -598,7 +598,7 @@ class TestLocalMachine:
 
         cmd = bash["-ce", "for ((i=0;1==1;i++)); do echo $i; sleep $i; done"]
 
-        with pytest.raises(ProcessLineTimedOut):
+        with pytest.raises(ProcessLineTimedOut):  # noqa: PT012
             # Order is important on mac
             for i, (out, err) in enumerate(cmd.popen().iter_lines(line_timeout=0.2)):
                 print(i, "out:", out)
@@ -627,7 +627,7 @@ class TestLocalMachine:
 
         result = echo["This is fun"] & TEE
         assert result[1] == "This is fun\n"
-        assert "This is fun\n" == capfd.readouterr()[0]
+        assert capfd.readouterr()[0] == "This is fun\n"
 
     @skip_on_windows
     def test_tee_race(self, capfd):
@@ -637,11 +637,11 @@ class TestLocalMachine:
         for _ in range(5):
             result = seq["1", "5000"] & TEE
             assert result[1] == EXPECT
-            assert EXPECT == capfd.readouterr()[0]
+            assert capfd.readouterr()[0] == EXPECT
 
     @skip_on_windows
     @pytest.mark.parametrize(
-        "modifier, expected",
+        ("modifier", "expected"),
         [
             (FG, None),
             (TF(FG=True), True),
@@ -809,7 +809,7 @@ class TestLocalMachine:
         assert list(local.pgrep("[pP]ython"))
 
     def _generate_sigint(self):
-        with pytest.raises(KeyboardInterrupt):
+        with pytest.raises(KeyboardInterrupt):  # noqa: PT012
             if sys.platform == "win32":
                 from win32api import GenerateConsoleCtrlEvent
 
@@ -1019,7 +1019,7 @@ for _ in range({}):
 
         result = echo["This is fun"].run_tee()
         assert result[1] == "This is fun\n"
-        assert "This is fun\n" == capfd.readouterr()[0]
+        assert capfd.readouterr()[0] == "This is fun\n"
 
     def test_run_tf(self):
         from plumbum.cmd import ls
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 30d14d0..7d49658 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -59,7 +59,7 @@ def test_connection():
     SshMachine(TEST_HOST)
 
 
-def test_incorrect_login(sshpass):
+def test_incorrect_login(sshpass):  # noqa: ARG001
     with pytest.raises(IncorrectLogin):
         SshMachine(
             TEST_HOST,
@@ -74,7 +74,7 @@ def test_incorrect_login(sshpass):
 
 
 @pytest.mark.xfail(env.LINUX, reason="TODO: no idea why this fails on linux")
-def test_hostpubkey_unknown(sshpass):
+def test_hostpubkey_unknown(sshpass):  # noqa: ARG001
     with pytest.raises(HostPublicKeyUnknown):
         SshMachine(
             TEST_HOST,
@@ -91,18 +91,18 @@ class TestRemotePath:
     def test_name(self):
         name = RemotePath(self._connect(), "/some/long/path/to/file.txt").name
         assert isinstance(name, str)
-        assert "file.txt" == str(name)
+        assert str(name) == "file.txt"
 
     def test_dirname(self):
         name = RemotePath(self._connect(), "/some/long/path/to/file.txt").dirname
         assert isinstance(name, RemotePath)
-        assert "/some/long/path/to" == str(name)
+        assert str(name) == "/some/long/path/to"
 
     def test_uri(self):
         p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt")
-        assert "ftp://" == p1.as_uri("ftp")[:6]
-        assert "ssh://" == p1.as_uri("ssh")[:6]
-        assert "/some/long/path/to/file.txt" == p1.as_uri()[-27:]
+        assert p1.as_uri("ftp")[:6] == "ftp://"
+        assert p1.as_uri("ssh")[:6] == "ssh://"
+        assert p1.as_uri()[-27:] == "/some/long/path/to/file.txt"
 
     def test_stem(self):
         p = RemotePath(self._connect(), "/some/long/path/to/file.txt")
@@ -148,15 +148,14 @@ class TestRemotePath:
 
     @skip_without_chown
     def test_chown(self):
-        with self._connect() as rem:
-            with rem.tempdir() as dir:
-                p = dir / "foo.txt"
-                p.write(b"hello")
-                # because we're connected to localhost, we expect UID and GID to be the same
-                assert p.uid == os.getuid()
-                assert p.gid == os.getgid()
-                p.chown(p.uid.name)
-                assert p.uid == os.getuid()
+        with self._connect() as rem, rem.tempdir() as dir:
+            p = dir / "foo.txt"
+            p.write(b"hello")
+            # because we're connected to localhost, we expect UID and GID to be the same
+            assert p.uid == os.getuid()
+            assert p.gid == os.getgid()
+            p.chown(p.uid.name)
+            assert p.uid == os.getuid()
 
     def test_parent(self):
         p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt")
@@ -182,7 +181,7 @@ class TestRemotePath:
             assert not tmp.exists()
 
     @pytest.mark.xfail(
-        reason="mkdir's mode argument is not yet implemented " "for remote paths",
+        reason="mkdir's mode argument is not yet implemented for remote paths",
         strict=True,
     )
     def test_mkdir_mode(self):
@@ -229,7 +228,6 @@ class TestRemotePath:
 
         with self._connect() as rem:
             with rem.tempdir() as tmp:
-
                 # setup a file and make sure it exists...
                 (tmp / "file_a").touch()
                 assert (tmp / "file_a").exists()
@@ -322,20 +320,22 @@ s.close()
                 rem["pwd"]()
 
     def test_glob(self):
-        with self._connect() as rem:
-            with rem.cwd(os.path.dirname(os.path.abspath(__file__))):
-                filenames = [f.name for f in rem.cwd // ("*.py", "*.bash")]
-                assert "test_remote.py" in filenames
-                assert "slow_process.bash" in filenames
+        with self._connect() as rem, rem.cwd(
+            os.path.dirname(os.path.abspath(__file__))
+        ):
+            filenames = [f.name for f in rem.cwd // ("*.py", "*.bash")]
+            assert "test_remote.py" in filenames
+            assert "slow_process.bash" in filenames
 
     def test_glob_spaces(self):
-        with self._connect() as rem:
-            with rem.cwd(os.path.dirname(os.path.abspath(__file__))):
-                filenames = [f.name for f in rem.cwd // ("*space.txt")]
-                assert "file with space.txt" in filenames
+        with self._connect() as rem, rem.cwd(
+            os.path.dirname(os.path.abspath(__file__))
+        ):
+            filenames = [f.name for f in rem.cwd // ("*space.txt")]
+            assert "file with space.txt" in filenames
 
-                filenames = [f.name for f in rem.cwd // ("*with space.txt")]
-                assert "file with space.txt" in filenames
+            filenames = [f.name for f in rem.cwd // ("*with space.txt")]
+            assert "file with space.txt" in filenames
 
     def test_cmd(self):
         with self._connect() as rem:
@@ -433,11 +433,11 @@ s.close()
 
     def test_iter_lines_error(self):
         with self._connect() as rem:
-            with pytest.raises(ProcessExecutionError) as ex:
+            with pytest.raises(ProcessExecutionError) as ex:  # noqa: PT012
                 for i, _lines in enumerate(rem["ls"]["--bla"].popen()):  # noqa: B007
                     pass
                 assert i == 1
-            assert "/bin/ls: " in ex.value.stderr
+            assert "ls: " in ex.value.stderr
 
     def test_touch(self):
         with self._connect() as rem:
@@ -466,7 +466,6 @@ class TestRemoteMachine(BaseRemoteMachineTest):
 
     @pytest.mark.parametrize("dynamic_lport", [False, True])
     def test_tunnel(self, dynamic_lport):
-
         for tunnel_prog in (self.TUNNEL_PROG_AF_INET, self.TUNNEL_PROG_AF_UNIX):
             with self._connect() as rem:
                 p = (rem.python["-u"] << tunnel_prog).popen()
@@ -477,10 +476,7 @@ class TestRemoteMachine(BaseRemoteMachineTest):
                 except ValueError:
                     dhost = None
 
-                if not dynamic_lport:
-                    lport = 12222
-                else:
-                    lport = 0
+                lport = 12222 if not dynamic_lport else 0
 
                 with rem.tunnel(lport, port_or_socket, dhost=dhost) as tun:
                     if not dynamic_lport:
@@ -501,7 +497,6 @@ class TestRemoteMachine(BaseRemoteMachineTest):
 
     @pytest.mark.parametrize("dynamic_dport", [False, True])
     def test_reverse_tunnel(self, dynamic_dport):
-
         lport = 12223 + dynamic_dport
         with self._connect() as rem:
             queue = Queue()
@@ -510,7 +505,6 @@ class TestRemoteMachine(BaseRemoteMachineTest):
             message = str(time.time())
 
             if not dynamic_dport:
-
                 get_unbound_socket_remote = """import sys, socket
 s = socket.socket()
 s.bind(("", 0))
diff --git a/tests/test_terminal.py b/tests/test_terminal.py
index 5a1fd70..2ad6a3a 100644
--- a/tests/test_terminal.py
+++ b/tests/test_terminal.py
@@ -102,7 +102,7 @@ class TestTerminal:
 
     def test_choose_dict(self):
         with send_stdin("23\n1"):
-            value = choose("Pick", dict(one="a", two="b"))
+            value = choose("Pick", {"one": "a", "two": "b"})
             assert value in ("a", "b")
 
     def test_ordered_dict(self):
diff --git a/tests/test_typed_env.py b/tests/test_typed_env.py
index b09b0c9..076842d 100644
--- a/tests/test_typed_env.py
+++ b/tests/test_typed_env.py
@@ -11,7 +11,7 @@ class TestTypedEnv:
             I = TypedEnv.Int("INT INTEGER".split())  # noqa: E741  # noqa: E741
             INTS = TypedEnv.CSV("CS_INTS", type=int)
 
-        raw_env = dict(TERM="xterm", CS_INTS="1,2,3,4")
+        raw_env = {"TERM": "xterm", "CS_INTS": "1,2,3,4"}
         e = E(raw_env)
 
         assert e.terminal == "xterm"
@@ -35,25 +35,25 @@ class TestTypedEnv:
         e.B = False
         assert raw_env["BOOL"] == "no"
 
-        assert e.INTS == [1, 2, 3, 4]
+        assert [1, 2, 3, 4] == e.INTS
         e.INTS = [1, 2]
-        assert e.INTS == [1, 2]
+        assert [1, 2] == e.INTS
         e.INTS = [1, 2, 3, 4]
 
         with pytest.raises(KeyError):
-            e.I
+            e.I  # noqa: B018
 
         raw_env["INTEGER"] = "4"
-        assert e.I == 4  # noqa: E741
+        assert e.I == 4
         assert e["I"] == 4
 
-        e.I = "5"  # noqa: E741
+        e.I = "5"
         assert raw_env["INT"] == "5"
-        assert e.I == 5  # noqa: E741
+        assert e.I == 5
         assert e["I"] == 5
 
         assert "{I} {B} {terminal}".format(**e) == "5 False foo"
-        assert dict(e) == dict(I=5, B=False, terminal="foo", INTS=[1, 2, 3, 4])
+        assert dict(e) == {"I": 5, "B": False, "terminal": "foo", "INTS": [1, 2, 3, 4]}
 
         r = TypedEnv(raw_env)
         assert "{INT} {BOOL} {TERM}".format(**r) == "5 no foo"
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b0a317a..53ea22f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -6,7 +6,7 @@ from plumbum.path.utils import copy, delete, move
 
 
 @skip_on_windows
-@pytest.mark.ssh
+@pytest.mark.ssh()
 def test_copy_move_delete():
     from plumbum.cmd import touch
 
@@ -26,31 +26,29 @@ def test_copy_move_delete():
         s2 = sorted(f.name for f in (dir / "dup").walk())
         assert s1 == s2
 
-        with SshMachine("localhost") as rem:
-            with rem.tempdir() as dir2:
-                copy(dir / "orig", dir2)
-                s3 = sorted(f.name for f in (dir2 / "orig").walk())
-                assert s1 == s3
+        with SshMachine("localhost") as rem, rem.tempdir() as dir2:
+            copy(dir / "orig", dir2)
+            s3 = sorted(f.name for f in (dir2 / "orig").walk())
+            assert s1 == s3
 
-                copy(dir2 / "orig", dir2 / "dup")
-                s4 = sorted(f.name for f in (dir2 / "dup").walk())
-                assert s1 == s4
+            copy(dir2 / "orig", dir2 / "dup")
+            s4 = sorted(f.name for f in (dir2 / "dup").walk())
+            assert s1 == s4
 
-                copy(dir2 / "dup", dir / "dup2")
-                s5 = sorted(f.name for f in (dir / "dup2").walk())
-                assert s1 == s5
+            copy(dir2 / "dup", dir / "dup2")
+            s5 = sorted(f.name for f in (dir / "dup2").walk())
+            assert s1 == s5
 
-                with SshMachine("localhost") as rem2:
-                    with rem2.tempdir() as dir3:
-                        copy(dir2 / "dup", dir3)
-                        s6 = sorted(f.name for f in (dir3 / "dup").walk())
-                        assert s1 == s6
+            with SshMachine("localhost") as rem2, rem2.tempdir() as dir3:
+                copy(dir2 / "dup", dir3)
+                s6 = sorted(f.name for f in (dir3 / "dup").walk())
+                assert s1 == s6
 
-                        move(dir3 / "dup", dir / "superdup")
-                        assert not (dir3 / "dup").exists()
+                move(dir3 / "dup", dir / "superdup")
+                assert not (dir3 / "dup").exists()
 
-                        s7 = sorted(f.name for f in (dir / "superdup").walk())
-                        assert s1 == s7
+                s7 = sorted(f.name for f in (dir / "superdup").walk())
+                assert s1 == s7
 
-                        # test rm
-                        delete(dir)
+                # test rm
+                delete(dir)
diff --git a/tests/test_validate.py b/tests/test_validate.py
index a39b8ca..4dbbe7b 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -5,7 +5,7 @@ class TestValidator:
     def test_named(self):
         class Try:
             @cli.positional(x=abs, y=str)
-            def main(selfy, x, y):  # noqa: B902
+            def main(selfy, x, y):
                 pass
 
         assert Try.main.positional == [abs, str]
@@ -14,7 +14,7 @@ class TestValidator:
     def test_position(self):
         class Try:
             @cli.positional(abs, str)
-            def main(selfy, x, y):  # noqa: B902
+            def main(selfy, x, y):
                 pass
 
         assert Try.main.positional == [abs, str]
@@ -23,7 +23,7 @@ class TestValidator:
     def test_mix(self):
         class Try:
             @cli.positional(abs, str, d=bool)
-            def main(selfy, x, y, z, d):  # noqa: B902
+            def main(selfy, x, y, z, d):
                 pass
 
         assert Try.main.positional == [abs, str, None, bool]
@@ -32,7 +32,7 @@ class TestValidator:
     def test_var(self):
         class Try:
             @cli.positional(abs, str, int)
-            def main(selfy, x, y, *g):  # noqa: B902
+            def main(selfy, x, y, *g):
                 pass
 
         assert Try.main.positional == [abs, str]
@@ -41,7 +41,7 @@ class TestValidator:
     def test_defaults(self):
         class Try:
             @cli.positional(abs, str)
-            def main(selfy, x, y="hello"):  # noqa: B902
+            def main(selfy, x, y="hello"):
                 pass
 
         assert Try.main.positional == [abs, str]
@@ -56,7 +56,7 @@ class TestProg:
 
         _, rc = MainValidator.run(["prog", "1", "2", "3", "4", "5"], exit=False)
         assert rc == 0
-        assert "1 2 (3, 4, 5)" == capsys.readouterr()[0].strip()
+        assert capsys.readouterr()[0].strip() == "1 2 (3, 4, 5)"
 
     def test_failure(self, capsys):
         class MainValidator(cli.Application):
@@ -80,8 +80,8 @@ class TestProg:
 
         _, rc = MainValidator.run(["prog", "1"], exit=False)
         assert rc == 0
-        assert "1 2" == capsys.readouterr()[0].strip()
+        assert capsys.readouterr()[0].strip() == "1 2"
 
         _, rc = MainValidator.run(["prog", "1", "3"], exit=False)
         assert rc == 0
-        assert "1 3" == capsys.readouterr()[0].strip()
+        assert capsys.readouterr()[0].strip() == "1 3"

More details

Full run details

Historical runs