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"