plumbum-1.8.3/.editorconfig0000644000000000000000000000016614613634536012625 0ustar00root = true [*.py] charset = utf-8 indent_style = space indent_size = 4 insert_final_newline = true end_of_line = lf plumbum-1.8.3/.gitattributes0000644000000000000000000000004314613634536013035 0ustar00*.py text eol=lf *.rst text eol=lf plumbum-1.8.3/.pre-commit-config.yaml0000644000000000000000000000246114613634536014431 0ustar00ci: autoupdate_commit_msg: "chore: update pre-commit hooks" autofix_commit_msg: "style: pre-commit fixes" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v4.6.0" hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-yaml exclude: ^conda.recipe/meta.yaml$ - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.4.2" hooks: - id: ruff args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: "v1.10.0" hooks: - id: mypy files: plumbum args: [] additional_dependencies: [typed-ast, types-paramiko, types-setuptools, pytest] - repo: https://github.com/abravalheri/validate-pyproject rev: "v0.16" hooks: - id: validate-pyproject - repo: https://github.com/codespell-project/codespell rev: "v2.2.6" hooks: - id: codespell args: ["-w"] additional_dependencies: [tomli] exclude: "(^pyproject.toml|.po)$" - repo: https://github.com/pre-commit/pygrep-hooks rev: "v1.10.0" hooks: - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal plumbum-1.8.3/.readthedocs.yml0000644000000000000000000000065114613634536013235 0ustar00# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Include PDF and ePub formats: all build: os: "ubuntu-22.04" tools: python: "3.8" python: install: - method: pip path: . extra_requirements: - docs plumbum-1.8.3/CHANGELOG.rst0000644000000000000000000005374514613634536012204 0ustar001.8.3 ----- * Fix StdinDataRedirection's formulate() method by @nebbish in https://github.com/tomerfiliba/plumbum/pull/629 * Missing non-zero return code from TEE'd pipe by @vient in https://github.com/tomerfiliba/plumbum/pull/661 * Fix stalling in Pipeline command by @astaric in https://github.com/tomerfiliba/plumbum/pull/632 * Use high-speed method for C locale too by @henryiii in https://github.com/tomerfiliba/plumbum/pull/681 * Update index.rst - fix broken link for sh project by @mcint in https://github.com/tomerfiliba/plumbum/pull/658 1.8.2 ----- * Fix author metadata on PyPI package and add static check (`#648 `_) * Add testing for Python 3.12 beta 1 (`#650 `_) * Use Ruff for linting (`#643 `_) * Paths: Add type hinting for Path (`#646 `_) 1.8.1 ----- * Accept path-like objects (`#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 `_) 1.8.0 ----- * Drop Python 2.7 and 3.5 support, add 3.11 support (`#573 `_) * Lots of extended checks and fixes for problems exposed. * Color: support ``NO_COLOR``/``FORCE_COLOR`` (`#575 `_) * Commands: New ``iter_lines`` ``buffer_size`` parameter (`#582 `_) * Commands: cache remote commands (`#583 `_) * SSH: Support reverse tunnels and dynamically allocated ports (`#608 `_) * CLI: add ``Set(..., all_markers={"*", "all"})`` and fix support for other separators (`#619 `_) * CLI: support future annotations (`#621 `_) * Color: fix the ABC (`#617 `_) * Exceptions: fix for exception pickling (`#586 `_) * Fix for StdinDataRedirection and modifiers (`#605 `_) 1.7.2 ----- * Commands: avoid issue mktemp issue on some BSD variants (`#571 `_) * Better specification of dependency on pywin32 (`#568 `_) * Some DeprecationWarnings changed to FutureWarnings (`#567 `_) 1.7.1 ----- * Paths: glob with local paths no longer expands the existing path too (`#552 `_) * Paramiko: support reverse tunnels (`#562 `_) * SSHMachine: support forwarding Unix sockets in ``.tunnel()`` (`#550 `_) * CLI: Support ``COLOR_GROUP_TITLES`` (`#553 `_) * Fix a deprecated in Python 3.10 warning (`#563 `_) * Extended testing and checking on Python 3.10 and various PyPy versions. Nox is supported for easier new-user development. 1.7.0 ----- * Commands: support ``.with_cwd()`` (`#513 `_) * Commands: make ``iter_lines`` deal with decoding errors during iteration (`#525 `_) * Commands: fix handling of env-vars passed to plumbum BoundEnvCommands (`#513 `_) * Commands: fix support for win32 in ``iter_lines`` (`#500 `_) * Paths: fix incorrect ``__getitem__`` method in Path (`#506 `_) * Paths: Remote path stat had odd OSError (`#505 `_) * Paths: Fix ``RemotePath.copy()`` (`#527 `_) * Paths: missing ``__fspath__`` added (`#498 `_) * SSH: better error reporting on SshSession error (`#515 `_) * Internal: redesigned CI, major cleanup to setuptools distribution, Black formatting, style checking throughout. 1.6.9 ----- * Last version to support Python 2.6; added python_requires for future versions (`#507 `_) * Paths: Fix bug with subscription operations (`#498 `_), (`#506 `_) * Paths: Fix resolve (`#492 `_) * Commands: Fix resolve (`#491 `_) * Commands: Add context manager on popen (`#495 `_) * Several smaller fixes (`#500 `_), (`#505 `_) 1.6.8 ----- * Exceptions: Changed ProcessExecutionError's formatting to be more user-friendly (`#456 `_) * Commands: support for per-line timeout with iter_lines (`#454 `_) * Commands: support for piping stdout/stderr to a logger (`#454 `_) * Paths: support composing paths using subscription operations (`#455 `_) * CLI: Improved 'Set' validator to allow non-string types, and CSV params (`#452 `_) * TypedEnv: Facility for modeling environment-variables into python data types (`#451 `_) * Commands: execute local/remote commands via a magic ``.cmd`` attribute (`#450 `_) 1.6.7 ----- * Commands: Added ``run_*`` methods as an alternative to modifiers (`#386 `_) * CLI: Added support for ``ALLOW_ABREV`` (`#401 `_) * CLI: Added ``DESCRIPTION_MORE``, preserves spacing (`#378 `_) * Color: Avoid throwing error in atexit in special cases (like pytest) (`#393 `_) * Including Python 3.7 in testing matrix. * Smaller bugfixes and other testing improvements. 1.6.6 ----- * Critical Bugfix: High-speed (English) translations could break the CLI module (`#371 `_) * Small improvement to wheels packaging 1.6.5 ----- * Critical Bugfix: Syntax error in image script could break pip installs (`#366 `_) * CLI: Regression fix: English apps now load as fast as in 1.6.3 (`#364 `_) * CLI: Missing colon restored in group names * Regression fix: Restored non-setuptools installs (but really, why would you not have setuptools?) (`#367 `_) 1.6.4 ----- * CLI: Support for localization (`#339 `_), with: - Russian by Pavel Pletenev (`#339 `_) 🇷🇺 - Dutch by Roel Aaij (`#351 `_) 🇳🇱 - French by Joel Closier (`#352 `_) 🇫🇷 - German by Christoph Hasse (`#353 `_) 🇩🇪 - Pulls with more languages welcome! * CLI: Support for ``MakeDirectory`` (`#339 `_) * Commands: Fixed unicode input/output on Python 2 (`#341 `_) * Paths: More updates for pathlib compatibility (`#325 `_) * Terminal: Changed ``prompt()``'s default value for ``type`` parameter from ``int`` to ``str`` to match existing docs (`#327 `_) * Remote: Support ``~`` in PATH for a remote (`#293 `_) * Remote: Fixes for globbing with spaces in filename on a remote server (`#322 `_) * Color: Fixes to image plots, better separation (`#324 `_) * Python 3.3 has been removed from Travis and Appveyor. * Several bugs fixed 1.6.3 ----- * Python 3.6 is now supported, critical bug fixed (`#302 `_) * Commands: Better handling of return codes for pipelines (`#288 `_) * Paths: Return split support (regression) (`#286 `_) - also supports dummy args for better ``str`` compatibility * Paths: Added support for Python 3.6 path protocol * Paths: Support Python's ``in`` syntax * CLI: Added Config parser (provisional) (`#304 `_) * Color: image plots with ``python -m plumbum.cli.image`` (`#304 `_) * SSH: No longer hangs for ``timeout`` seconds on failure (`#306 `_) * Test improvements, especially on non-linux systems 1.6.2 ----- * CLI: ``Progress`` now has a clear keyword that hides the bar on completion * CLI: ``Progress`` without clear now starts on next line without having to manually add ``\n``. * Commands: modifiers now accept a timeout parameter (`#281 `_) * Commands: ``BG`` modifier now allows ``stdout``/``stderr`` redirection (to screen, for example) (`#258 `_) * Commands: Modifiers no longer crash on repr (see `#262 `_) * Remote: ``nohup`` works again, typo fixed (`#261 `_) * Added better support for SunOS and other OS's. (`#260 `_) * Colors: Context manager flushes stream now, provides more consistent results * Other smaller bugfixes, better support for Python 3.6+ 1.6.1 ----- * CLI: ``Application`` subclasses can now be run directly, instead of calling ``.run()``, to facilitate using as entry points (`#237 `_) * CLI: ``gui_open`` added to allow easy opening of paths in default gui editor (`#239 `_) * CLI: More control over help message (`#233 `_) * Remote: ``cwd`` is now stashed to reduce network usage (similar to Plumbum <1.6 behavior), and absolute paths are faster, (`#238 `_) * Bugfix: Pipelined return codes now give correct attribution (`#243 `_) * Bugfix: ``Progress`` works on Python 2.6 (`#230 `_) * Bugfix: Colors now work with more terminals (`#231 `_) * Bugfix: Getting an executable no longer returns a directory (`#234 `_) * Bugfix: Iterdir now works on Python <3.5 * Testing is now expanded and fully written in Pytest, with coverage reporting. * Added support for Conda ( as of 1.6.2, use the ``-c conda-forge`` channel) 1.6.0 ----- * Added support for Python 3.5, PyPy, and better Windows and Mac support, with CI testing (`#218 `_, `#217 `_, `#226 `_) * Colors: Added colors module, support for colors added to cli (`#213 `_) * Machines: Added ``.get()`` method for checking several commands. (`#205 `_) * Machines: ``local.cwd`` now is the current directory even if you change the directory with non-Plumbum methods (fixes unexpected behavior). (`#207 `_) * SSHMachine: Better error message for SSH (`#211 `_) * SSHMachine: Support for FreeBSD remote (`#220 `_) * Paths: Now a subclass of ``str``, can be opened directly (`#228 `_) * Paths: Improved pathlib compatibility with several additions and renames (`#223 `_) * Paths: Added globbing multiple patterns at once (`#221 `_) * Commands: added ``NOHUP`` modifier (`#221 `_) * CLI: added positional argument validation (`#225 `_) * CLI: added ``envname``, which allows you specify an environment variable for a ``SwitchAttr`` (`#216 `_) * CLI terminal: added ``Progress``, a command line progress bar for iterators and ranges (`#214 `_) * Continued to clean out Python 2.5 hacks 1.5.0 ----- * Removed support for Python 2.5. (Travis CI does not support it anymore) * CLI: add ``invoke``, which allows you to programmatically run applications (`#149 `_) * CLI: add ``--help-all`` and various cosmetic fixes: (`#125 `_), (`#126 `_), (`#127 `_) * CLI: add ``root_app`` property (`#141 `_) * Machines: ``getattr`` now raises ``AttributeError`` instead of ``CommandNotFound`` (`#135 `_) * Paramiko: ``keep_alive`` support (`#186 `_) * Paramiko: does not support piping explicitly now (`#160 `_) * Parmaiko: Added pure SFTP backend, gives STFP v4+ support (`#188 `_) * Paths: bugfix to ``cwd`` interaction with ``Path`` (`#142 `_) * Paths: read/write now accept an optional encoding parameter (`#148 `_) * Paths: Suffix support similar to the Python 3.4 standard library ``pathlib`` (`#198 `_) * Commands: renamed ``setenv`` to ``with_env`` (`#143 `_) * Commands: pipelines will now fail with ``ProcessExecutionError`` if the source process fails (`#145 `_) * Commands: added ``TF`` and ``RETCODE`` modifiers (`#202 `_) * Experimental concurrent machine support in ``experimental/parallel.py`` * Several minor bug fixes, including Windows and Python 3 fixes (`#199 `_, `#195 `_) 1.4.2 ----- * Paramiko now supports Python 3, enabled support in Plumbum * Terminal: added ``prompt()``, bugfix to ``get_terminal_size()`` (`#113 `_) * CLI: added ``cleanup()``, which is called after ``main()`` returns * CLI: bugfix to ``CountOf`` (`#118 `_) * Commands: Add a TEE modifier (`#117 `_) * Remote machines: bugfix to ``which``, bugfix to remote environment variables (`#122 `_) * Path: ``read()``/``write()`` now operate on bytes 1.4.1 ----- * Force ``/bin/sh`` to be the shell in ``SshMachine.session()`` (`#111 `_) * Added ``islink()`` and ``unlink()`` to path objects (`#100 `_, `#103 `_) * Added ``access`` to path objects * Faster ``which`` implementation (`#98 `_) * Several minor bug fixes 1.4 --- * Moved ``atomic`` and ``unixutils`` into the new ``fs`` package (file-system related utilities) * Dropped ``plumbum.utils`` legacy shortcut in favor of ``plumbum.path.utils`` * Bugfix: the left-hand-side process of a pipe wasn't waited on, leading to zombies (`#89 `_) * Added ``RelativePath`` (the result of ``Path.relative_to``) * Fixed more text alignment issues in ``cli.Application.help()`` * Introduced ``ask()`` and ``choose`` to ``cli.terminal`` * Bugfix: Path comparison operators were wrong * Added connection timeout to ``RemoteMachine`` 1.3 --- * ``Command.popen``: a new argument, ``new_session`` may be passed to ``Command.popen``, which runs the given in a new session (``setsid`` on POSIX, ``CREATE_NEW_PROCESS_GROUP`` on Windows) * ``Command.Popen``: args can now also be a list (previously, it was required to be a tuple). See * ``local.daemonize``: run commands as full daemons (double-fork and ``setsid``) on POSIX systems, or detached from their controlling console and parent (on Windows). * ``list_processes``: return a list of running process (local/remote machines) * ``SshMachine.nohup``: "daemonize" remote commands via ``nohup`` (not really a daemon, but good enough) * ``atomic``: Atomic file operations (``AtomicFile``, ``AtomicCounterFile`` and ``PidFile``) * ``copy`` and ``move``: the ``src`` argument can now be a list of files to move, e.g., ``copy(["foo", "bar"], "/usr/bin")`` * list local and remote processes * cli: better handling of text wrapping in the generated help message * cli: add a default ``main()`` method that checks for unknown subcommands * terminal: initial commit (``get_terminal_size``) * packaging: the package was split into subpackages; it grew too big for a flat namespace. imports are not expected to be broken by this change * SshMachine: added ``password`` parameter, which relies on `sshpass `_ to feed the password to ``ssh``. This is a security risk, but it's occasionally necessary. Use this with caution! * Commands now have a ``machine`` attribute that points to the machine they run on * Commands gained ``setenv``, which creates a command with a bound environment * Remote path: several fixes to ``stat`` (``StatRes``) * cli: add lazily-loaded subcommands (e.g., ``MainApp.subcommand("foo", "my.package.foo.FooApp")``), which are imported on demand * Paths: added `relative_to and split `_, which (respectively) computes the difference between two paths and splits paths into lists of nodes * cli: ``Predicate`` became a class decorator (it exists solely for pretty-printing anyway) * PuttyMachine: `bugfix `_ 1.2 --- * Path: added `chmod `_ * Path: added `link and symlink `_ * Path: ``walk()`` now applies filter recursively (`#64 `_) * Commands: added `Append redirect `_ * Commands: fix ``_subprocess`` issue (`#59 `_) * Commands: add ``__file__`` to module hack (`#66 `_) * Paramiko: add `'username' and 'password' `_ * Paramiko: add `'timeout' and 'look_for_keys' `_ * Python 3: fix `#56 `_ and `#55 `_ 1.1 --- * `Paramiko `_ integration (`#10 `_) * CLI: now with built-in support for `sub-commands `_. See also: `#43 `_ * The "import hack" has moved to the package's ``__init__.py``, to make it importable directly (`#45 `_) * Paths now support ``chmod`` (on POSIX platform) (`#49 `_) * The argument name of a ``SwitchAttr`` can now be given to it (defaults to ``VALUE``) (`#46 `_) 1.0.1 ----- * Windows: path are no longer converted to lower-case, but ``__eq__`` and ``__hash__`` operate on the lower-cased result (`#38 `_) * Properly handle empty strings in the argument list (`#41 `_) * Relaxed type-checking of ``LocalPath`` and ``RemotePath`` (`#35 `_) * Added ``PuttyMachine`` for Windows users that relies on ``plink`` and ``pscp`` (instead of ``ssh`` and ``scp``) `(#37 `_) 1.0.0 ----- * Rename ``cli.CountingAttr`` to ``cli.CountOf`` * Moved to `Travis `_ continuous integration * Added ``unixutils`` * Added ``chown`` and ``uid``/``gid`` * Lots of fixes and updates to the doc * Full list of `issues `_ 0.9.0 ----- Initial release plumbum-1.8.3/CONTRIBUTING.rst0000644000000000000000000000150314613634536012605 0ustar00Contributing to Plumbum ======================= General comments ---------------- Pull requests welcome! Please make sure you add tests (in an easy ``pytest`` format) to the tests folder for your fix or features. Make sure you add documentation covering a new feature. Adding a language ----------------- Plumbum.cli prints various messages for the user. These can be localized into your local language; pull requests adding languages are welcome. To add a language, run ``./translations.py`` from the main github directory, and then copy the file ``plumbum/cli/i18n/messages.pot`` to ``plumbum/cli/i18n/.po``, and add your language. Run ``./translations.py`` again to update the file you made (save first) and also create the needed files binary file. See `gettext: PMOTW3 `_ for more info. plumbum-1.8.3/noxfile.py0000644000000000000000000000267314613634536012173 0ustar00from __future__ import annotations import nox ALL_PYTHONS = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] nox.options.sessions = ["lint", "tests"] @nox.session(reuse_venv=True) def lint(session): """ Run the linter. """ session.install("pre-commit") session.run("pre-commit", "run", "--all-files", *session.posargs) @nox.session def pylint(session): """ Run pylint. """ session.install(".", "paramiko", "ipython", "pylint~=3.1.0") session.run("pylint", "plumbum", *session.posargs) @nox.session(python=ALL_PYTHONS, reuse_venv=True) def tests(session): """ Run the unit and regular tests. """ session.install("-e", ".[dev]") session.run("pytest", *session.posargs, env={"PYTHONTRACEMALLOC": "5"}) @nox.session(reuse_venv=True) def docs(session): """ Build the docs. Pass "serve" to serve. """ session.install("-e", ".[docs]") session.chdir("docs") session.run("sphinx-build", "-M", "html", ".", "_build") if session.posargs: if "serve" in session.posargs: session.log("Launching docs at http://localhost:8000/ - use Ctrl-C to quit") session.run("python", "-m", "http.server", "8000", "-d", "_build/html") else: session.log("Unsupported argument to docs") @nox.session def build(session): """ Build an SDist and wheel. """ session.install("build") session.run("python", "-m", "build") plumbum-1.8.3/setup.cfg0000644000000000000000000000044614613634536011772 0ustar00[coverage:run] branch = True relative_files = True 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__.: plumbum-1.8.3/translations.py0000755000000000000000000000205214613634536013242 0ustar00#!/usr/bin/env python3 # If you are on macOS and using brew, you might need the following first: # export PATH="/usr/local/opt/gettext/bin:$PATH" from plumbum import FG, local from plumbum.cmd import msgfmt, msgmerge, xgettext translation_dir = local.cwd / "plumbum/cli/i18n" template = translation_dir / "messages.pot" ( xgettext[ "--from-code", "utf-8", "-L", "python", "--keyword=T_", "--package-name=Plumbum.cli", "-o", template, sorted(x - local.cwd for x in local.cwd / "plumbum/cli" // "*.py"), ] & FG ) for translation in translation_dir // "*.po": lang = translation.stem new_tfile = translation.with_suffix(".po.new") # Merge changes to new file (msgmerge[translation, template] > new_tfile) & FG new_tfile.move(translation) # Render new file into runtime output local_dir = translation_dir / lang / "LC_MESSAGES" if not local_dir.exists(): local_dir.mkdir() msgfmt["-o", local_dir / "plumbum.cli.mo", translation] & FG plumbum-1.8.3/.github/dependabot.yml0000644000000000000000000000034014613634536014332 0ustar00version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: actions: patterns: - "*" plumbum-1.8.3/.github/matchers/pylint.json0000644000000000000000000000123414613634536015525 0ustar00{ "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" } ] } plumbum-1.8.3/.github/workflows/cd.yml0000644000000000000000000000121614613634536014653 0ustar00name: CD on: workflow_dispatch: release: types: - published env: FORCE_COLOR: 3 jobs: dist: name: Dist runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 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@v4 with: name: Packages path: dist - uses: pypa/gh-action-pypi-publish@release/v1 plumbum-1.8.3/.github/workflows/ci.yml0000644000000000000000000000563514613634536014671 0ustar00name: 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@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.x" - uses: pre-commit/action@v3.0.1 - 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"] os: [ubuntu-latest, windows-latest, macos-13] include: - python-version: 'pypy-3.8' os: ubuntu-latest - python-version: 'pypy-3.10' 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@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v4 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@v5 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 }} plumbum-1.8.3/conda.recipe/.gitignore0000644000000000000000000000010314613634536014461 0ustar00/linux-32/* /linux-64/* /osx-64/* /win-32/* /win-64/* /outputdir/* plumbum-1.8.3/conda.recipe/README.mkd0000644000000000000000000000146214613634536014134 0ustar00# Building instructions Change to the `conda.recipes` directory. Run ```bash $ conda install conda-build ``` to acquire the conda build tools. Then you can build with ```bash conda build --python 3.5 . ``` and pay attention to the output directory. You should see something that looks like ``` anaconda upload //anaconda/conda-bld/osx-64/plumbum-v1.6.3-py35_0.tar.bz2 ``` Now, you will need to convert to other architectures. On non-Windows systems: ``` conda convert --platform all //anaconda/conda-bld/osx-64/plumbum-v1.6.3-py35_0.tar.bz2 -o outputdir\ ``` and Windows users will need to add a `-f`. Rerun the following steps for all python versions. To upload packages, ```bash conda install anaconda-client anaconda login for f in `ls outputdir/*/*.tar.bz2`; do anaconda upload $f; done anaconda logout ``` plumbum-1.8.3/conda.recipe/bld.bat0000644000000000000000000000006314613634536013727 0ustar00"%PYTHON%" setup.py install if errorlevel 1 exit 1 plumbum-1.8.3/conda.recipe/build.sh0000644000000000000000000000004614613634536014132 0ustar00#!/bin/bash $PYTHON setup.py install plumbum-1.8.3/conda.recipe/meta.yaml0000644000000000000000000000161014613634536014307 0ustar00package: name: plumbum version: {{ environ.get('GIT_DESCRIBE_TAG', '').replace('v','') }} source: path: ../ requirements: build: - python - setuptools run: - python - paramiko build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} {% if environ.get('GIT_DESCRIBE_NUMBER', '0') == '0' %}string: py{{ environ.get('PY_VER').replace('.', '') }}_0 {% else %}string: py{{ environ.get('PY_VER').replace('.', '') }}_{{ environ.get('GIT_BUILD_STR', 'GIT_STUB') }}{% endif %} test: # Python imports imports: - plumbum - plumbum.cli - plumbum.colorlib - plumbum.commands - plumbum.fs - plumbum.machines - plumbum.path requires: # Put any additional test requirements here. For example - pytest - paramiko about: home: https://plumbum.readthedocs.io license: MIT License summary: 'Plumbum: shell combinators library' plumbum-1.8.3/docs/.gitignore0000644000000000000000000000001014613634536013054 0ustar00/_build plumbum-1.8.3/docs/Makefile0000644000000000000000000001300014613634536012527 0ustar00# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PlumbumShellCombinators.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PlumbumShellCombinators.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PlumbumShellCombinators" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PlumbumShellCombinators" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." plumbum-1.8.3/docs/_cheatsheet.rst0000644000000000000000000001317014613634536014105 0ustar00 Basics ------ .. code-block:: python >>> from plumbum import local >>> ls = local["ls"] >>> ls LocalCommand() >>> ls() 'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n' >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad() # Notepad window pops up '' # Notepad window is closed by user, command returns Instead of writing ``xxx = local["xxx"]`` for every program you wish to use, you can also :ref:`import commands `: >>> from plumbum.cmd import grep, wc, cat, head >>> grep LocalCommand() Or, use the ``local.cmd`` syntactic-sugar: .. code-block:: python >>> local.cmd.ls LocalCommand() >>> local.cmd.ls() 'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n' See :ref:`guide-local-commands`. Piping ------ .. code-block:: python >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> print(chain) /bin/ls -a | /bin/grep -v '\.py' | /usr/bin/wc -l >>> chain() '13\n' See :ref:`guide-local-commands-pipelining`. Redirection ----------- .. code-block:: python >>> ((cat < "setup.py") | head["-n", 4])() '#!/usr/bin/env python3\nimport os\n\ntry:\n' >>> (ls["-a"] > "file.list")() '' >>> (cat["file.list"] | wc["-l"])() '17\n' See :ref:`guide-local-commands-redir`. Working-directory manipulation ------------------------------ .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() ... '15\n' A more explicit, and thread-safe way of running a command in a different directory is using the ``.with_cwd()`` method:: .. code-block:: python >>> ls_in_docs = local.cmd.ls.with_cwd("docs") >>> ls_in_docs() 'api\nchangelog.rst\n_cheatsheet.rst\ncli.rst\ncolorlib.rst\n_color_list.html\ncolors.rst\nconf.py\nindex.rst\nlocal_commands.rst\nlocal_machine.rst\nmake.bat\nMakefile\n_news.rst\npaths.rst\nquickref.rst\nremote.rst\n_static\n_templates\ntyped_env.rst\nutils.rst\n' See :ref:`guide-paths` and :ref:`guide-local-machine`. Foreground and background execution ----------------------------------- .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep["\\.py"]) & FG # The output is printed to stdout directly build.py .pydevproject setup.py >>> (ls["-a"] | grep["\\.py"]) & BG # The process runs "in the background" See :ref:`guide-local-commands-bgfg`. Command nesting --------------- .. code-block:: python >>> from plumbum.cmd import sudo >>> 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 See :ref:`guide-local-commands-nesting`. Remote commands (over SSH) -------------------------- Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) and `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' See :ref:`guide-remote`. 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') See :ref:`guide-cli`. 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.") Sample output +++++++++++++ .. raw:: html
This library provides safe color access.
    Color (and styles in general) are easy!
    The simple 16 colors, 256 named colors, or full hex colors can be used.
    Unsafe color access is available too.
See :ref:`guide-colors`. plumbum-1.8.3/docs/_color_list.html0000644000000000000000000005430214613634536014277 0ustar00
  1. #000000 Black

  2. #C00000 Red

  3. #00C000 Green

  4. #C0C000 Yellow

  5. #0000C0 Blue

  6. #C000C0 Magenta

  7. #00C0C0 Cyan

  8. #C0C0C0 LightGray

  9. #808080 DarkGray

  10. #FF0000 LightRed

  11. #00FF00 LightGreen

  12. #FFFF00 LightYellow

  13. #0000FF LightBlue

  14. #FF00FF LightMagenta

  15. #00FFFF LightCyan

  16. #FFFFFF White

  17. #000000 Grey0

  18. #00005F NavyBlue

  19. #000087 DarkBlue

  20. #0000AF Blue3

  21. #0000D7 Blue3A

  22. #0000FF Blue1

  23. #005F00 DarkGreen

  24. #005F5F DeepSkyBlue4

  25. #005F87 DeepSkyBlue4A

  26. #005FAF DeepSkyBlue4B

  27. #005FD7 DodgerBlue3

  28. #005FFF DodgerBlue2

  29. #008700 Green4

  30. #00875F SpringGreen4

  31. #008787 Turquoise4

  32. #0087AF DeepSkyBlue3

  33. #0087D7 DeepSkyBlue3A

  34. #0087FF DodgerBlue1

  35. #00AF00 Green3

  36. #00AF5F SpringGreen3

  37. #00AF87 DarkCyan

  38. #00AFAF LightSeaGreen

  39. #00AFD7 DeepSkyBlue2

  40. #00AFFF DeepSkyBlue1

  41. #00D700 Green3A

  42. #00D75F SpringGreen3A

  43. #00D787 SpringGreen2

  44. #00D7AF Cyan3

  45. #00D7D7 DarkTurquoise

  46. #00D7FF Turquoise2

  47. #00FF00 Green1

  48. #00FF5F SpringGreen2A

  49. #00FF87 SpringGreen1

  50. #00FFAF MediumSpringGreen

  51. #00FFD7 Cyan2

  52. #00FFFF Cyan1

  53. #5F0000 DarkRed

  54. #5F005F DeepPink4

  55. #5F0087 Purple4

  56. #5F00AF Purple4A

  57. #5F00D7 Purple3

  58. #5F00FF BlueViolet

  59. #5F5F00 Orange4

  60. #5F5F5F Grey37

  61. #5F5F87 MediumPurple4

  62. #5F5FAF SlateBlue3

  63. #5F5FD7 SlateBlue3A

  64. #5F5FFF RoyalBlue1

  65. #5F8700 Chartreuse4

  66. #5F875F DarkSeaGreen4

  67. #5F8787 PaleTurquoise4

  68. #5F87AF SteelBlue

  69. #5F87D7 SteelBlue3

  70. #5F87FF CornflowerBlue

  71. #5FAF00 Chartreuse3

  72. #5FAF5F DarkSeaGreen4A

  73. #5FAF87 CadetBlue

  74. #5FAFAF CadetBlueA

  75. #5FAFD7 SkyBlue3

  76. #5FAFFF SteelBlue1

  77. #5FD700 Chartreuse3A

  78. #5FD75F PaleGreen3

  79. #5FD787 SeaGreen3

  80. #5FD7AF Aquamarine3

  81. #5FD7D7 MediumTurquoise

  82. #5FD7FF SteelBlue1A

  83. #5FFF00 Chartreuse2A

  84. #5FFF5F SeaGreen2

  85. #5FFF87 SeaGreen1

  86. #5FFFAF SeaGreen1A

  87. #5FFFD7 Aquamarine1

  88. #5FFFFF DarkSlateGray2

  89. #870000 DarkRedA

  90. #87005F DeepPink4A

  91. #870087 DarkMagenta

  92. #8700AF DarkMagentaA

  93. #8700D7 DarkViolet

  94. #8700FF Purple

  95. #875F00 Orange4A

  96. #875F5F LightPink4

  97. #875F87 Plum4

  98. #875FAF MediumPurple3

  99. #875FD7 MediumPurple3A

  100. #875FFF SlateBlue1

  101. #878700 Yellow4

  102. #87875F Wheat4

  103. #878787 Grey53

  104. #8787AF LightSlateGrey

  105. #8787D7 MediumPurple

  106. #8787FF LightSlateBlue

  107. #87AF00 Yellow4A

  108. #87AF5F DarkOliveGreen3

  109. #87AF87 DarkSeaGreen

  110. #87AFAF LightSkyBlue3

  111. #87AFD7 LightSkyBlue3A

  112. #87AFFF SkyBlue2

  113. #87D700 Chartreuse2

  114. #87D75F DarkOliveGreen3A

  115. #87D787 PaleGreen3A

  116. #87D7AF DarkSeaGreen3

  117. #87D7D7 DarkSlateGray3

  118. #87D7FF SkyBlue1

  119. #87FF00 Chartreuse1

  120. #87FF5F LightGreenA

  121. #87FF87 LightGreenB

  122. #87FFAF PaleGreen1

  123. #87FFD7 Aquamarine1A

  124. #87FFFF DarkSlateGray1

  125. #AF0000 Red3

  126. #AF005F DeepPink4B

  127. #AF0087 MediumVioletRed

  128. #AF00AF Magenta3

  129. #AF00D7 DarkVioletA

  130. #AF00FF PurpleA

  131. #AF5F00 DarkOrange3

  132. #AF5F5F IndianRed

  133. #AF5F87 HotPink3

  134. #AF5FAF MediumOrchid3

  135. #AF5FD7 MediumOrchid

  136. #AF5FFF MediumPurple2

  137. #AF8700 DarkGoldenrod

  138. #AF875F LightSalmon3

  139. #AF8787 RosyBrown

  140. #AF87AF Grey63

  141. #AF87D7 MediumPurple2A

  142. #AF87FF MediumPurple1

  143. #AFAF00 Gold3

  144. #AFAF5F DarkKhaki

  145. #AFAF87 NavajoWhite3

  146. #AFAFAF Grey69

  147. #AFAFD7 LightSteelBlue3

  148. #AFAFFF LightSteelBlue

  149. #AFD700 Yellow3

  150. #AFD75F DarkOliveGreen3B

  151. #AFD787 DarkSeaGreen3A

  152. #AFD7AF DarkSeaGreen2

  153. #AFD7D7 LightCyan3

  154. #AFD7FF LightSkyBlue1

  155. #AFFF00 GreenYellow

  156. #AFFF5F DarkOliveGreen2

  157. #AFFF87 PaleGreen1A

  158. #AFFFAF DarkSeaGreen2A

  159. #AFFFD7 DarkSeaGreen1

  160. #AFFFFF PaleTurquoise1

  161. #D70000 Red3A

  162. #D7005F DeepPink3

  163. #D70087 DeepPink3A

  164. #D700AF Magenta3A

  165. #D700D7 Magenta3B

  166. #D700FF Magenta2

  167. #D75F00 DarkOrange3A

  168. #D75F5F IndianRedA

  169. #D75F87 HotPink3A

  170. #D75FAF HotPink2

  171. #D75FD7 Orchid

  172. #D75FFF MediumOrchid1

  173. #D78700 Orange3

  174. #D7875F LightSalmon3A

  175. #D78787 LightPink3

  176. #D787AF Pink3

  177. #D787D7 Plum3

  178. #D787FF Violet

  179. #D7AF00 Gold3A

  180. #D7AF5F LightGoldenrod3

  181. #D7AF87 Tan

  182. #D7AFAF MistyRose3

  183. #D7AFD7 Thistle3

  184. #D7AFFF Plum2

  185. #D7D700 Yellow3A

  186. #D7D75F Khaki3

  187. #D7D787 LightGoldenrod2

  188. #D7D7AF LightYellow3

  189. #D7D7D7 Grey84

  190. #D7D7FF LightSteelBlue1

  191. #D7FF00 Yellow2

  192. #D7FF5F DarkOliveGreen1

  193. #D7FF87 DarkOliveGreen1A

  194. #D7FFAF DarkSeaGreen1A

  195. #D7FFD7 Honeydew2

  196. #D7FFFF LightCyan1

  197. #FF0000 Red1

  198. #FF005F DeepPink2

  199. #FF0087 DeepPink1

  200. #FF00AF DeepPink1A

  201. #FF00D7 Magenta2A

  202. #FF00FF Magenta1

  203. #FF5F00 OrangeRed1

  204. #FF5F5F IndianRed1

  205. #FF5F87 IndianRed1A

  206. #FF5FAF HotPink

  207. #FF5FD7 HotPinkA

  208. #FF5FFF MediumOrchid1A

  209. #FF8700 DarkOrange

  210. #FF875F Salmon1

  211. #FF8787 LightCoral

  212. #FF87AF PaleVioletRed1

  213. #FF87D7 Orchid2

  214. #FF87FF Orchid1

  215. #FFAF00 Orange1

  216. #FFAF5F SandyBrown

  217. #FFAF87 LightSalmon1

  218. #FFAFAF LightPink1

  219. #FFAFD7 Pink1

  220. #FFAFFF Plum1

  221. #FFD700 Gold1

  222. #FFD75F LightGoldenrod2A

  223. #FFD787 LightGoldenrod2B

  224. #FFD7AF NavajoWhite1

  225. #FFD7D7 MistyRose1

  226. #FFD7FF Thistle1

  227. #FFFF00 Yellow1

  228. #FFFF5F LightGoldenrod1

  229. #FFFF87 Khaki1

  230. #FFFFAF Wheat1

  231. #FFFFD7 Cornsilk1

  232. #FFFFFF Grey100

  233. #080808 Grey3

  234. #121212 Grey7

  235. #1C1C1C Grey11

  236. #262626 Grey15

  237. #303030 Grey19

  238. #3A3A3A Grey23

  239. #444444 Grey27

  240. #4E4E4E Grey30

  241. #585858 Grey35

  242. #626262 Grey39

  243. #6C6C6C Grey42

  244. #767676 Grey46

  245. #808080 Grey50

  246. #8A8A8A Grey54

  247. #949494 Grey58

  248. #9E9E9E Grey62

  249. #A8A8A8 Grey66

  250. #B2B2B2 Grey70

  251. #BCBCBC Grey74

  252. #C6C6C6 Grey78

  253. #D0D0D0 Grey82

  254. #DADADA Grey85

  255. #E4E4E4 Grey89

  256. #EEEEEE Grey93

plumbum-1.8.3/docs/_news.rst0000644000000000000000000000547414613634536012754 0ustar00* **2023.05.30**: Version 1.8.2 released with a PyPI metadata fix, Python 3.12b1 testing, and a bit more typing. * **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. * **2021.02.08**: Version 1.7.0 released with a few new features like ``.with_cwd``, some useful bugfixes, and lots of cleanup. * **2020.03.23**: Version 1.6.9 released with several Path fixes, final version to support Python 2.6. * **2019.10.30**: Version 1.6.8 released with ``local.cmd``, a few command updates, ``Set`` improvements, and ``TypedEnv``. * **2018.08.10**: Version 1.6.7 released with several minor additions, mostly to CLI apps, and ``run_*`` modifiers added. * **2018.02.12**: Version 1.6.6 released with one more critical bugfix for a error message regression in 1.6.5. * **2017.12.29**: Version 1.6.5 released with mostly bugfixes, including a critical one that could break pip installs on some platforms. English cli apps now load as fast as before the localization update. * **2017.11.27**: Version 1.6.4 released with new CLI localization support. Several bugfixes and better pathlib compatibility, along with better separation between Plumbum's internal packages. * **2016.12.31**: Version 1.6.3 released to provide Python 3.6 compatibility. Mostly bugfixes, several smaller improvements to paths, and a provisional config parser added. * **2016.12.3**: Version 1.6.2 is now available through `conda-forge `_, as well. * **2016.6.25**: Version 1.6.2 released. This is mostly a bug fix release, but a few new features are included. Modifiers allow some new arguments, and ``Progress`` is improved. Better support for SunOS and other OS's. * **2015.12.18**: Version 1.6.1 released. The release mostly contains smaller fixes for CLI, 2.6/3.5 support, and colors. PyTest is now used for tests, and Conda is supported. * **2015.10.16**: Version 1.6.0 released. Highlights include Python 3.5 compatibility, the ``plumbum.colors`` package, ``Path`` becoming a subclass of ``str`` and a host of bugfixes. Special thanks go to Henry for his efforts. * **2015.07.17**: Version 1.5.0 released. This release brings a host of bug fixes, code cleanups and some experimental new features (be sure to check the changelog). Also, say hi to `Henry Schreiner `_, who has joined as a member of the project. plumbum-1.8.3/docs/changelog.rst0000644000000000000000000000006514613634536013557 0ustar00Change Log ========== .. include:: ../CHANGELOG.rst plumbum-1.8.3/docs/cli.rst0000644000000000000000000006224214613634536012404 0ustar00.. _guide-cli: 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 `_; 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 translates these primitives into Pythonic constructs and relies on introspection. From a bird's eye view, CLI applications are classes that extend :class:`plumbum.cli.Application`. They define a ``main()`` method and optionally expose methods and attributes as command-line :func:`switches `. Switches may take arguments, and any remaining positional arguments are given to the ``main`` method, according to its signature. A simple CLI application might look like this:: from plumbum import cli class MyApp(cli.Application): verbose = cli.Flag(["v", "verbose"], help = "If given, I will be very talkative") def main(self, filename): print(f"I will now read {filename}") if self.verbose: print("Yadda " * 200) if __name__ == "__main__": MyApp.run() And you can run it:: $ python3 example.py foo I will now read foo $ python3 example.py --help example.py v1.0 Usage: example.py [SWITCHES] filename Meta-switches: -h, --help Prints this help message and quits --version Prints the program's version and quits Switches: -v, --verbose If given, I will be very talkative So far you've only seen the very basic usage. We'll now start to explore the library. .. versionadded:: 1.6.1 You can also directly run the app, as ``MyApp()``, without arguments, instead of calling ``.main()``. Application ----------- The :class:`Application ` class is the "container" of your application. It consists of the ``main()`` method, which you should implement, and any number of CLI-exposed switch functions or attributes. The entry-point for your application is the classmethod ``run``, which instantiates your class, parses the arguments, invokes all switch functions, and then calls ``main()`` with the given positional arguments. In order to run your application from the command-line, all you have to do is :: if __name__ == "__main__": MyApp.run() Aside from ``run()`` and ``main()``, the ``Application`` class exposes two built-in switch functions: ``help()`` and ``version()`` which take care of displaying the help and program's version, respectively. By default, ``--help`` and ``-h`` invoke ``help()``, and ``--version`` and ``-v`` invoke ``version()``; if any of these functions is called, the application will display the message and quit (without processing any other switch). You can customize the information displayed by ``help()`` and ``version`` by defining class-level attributes, such as ``PROGNAME``, ``VERSION`` and ``DESCRIPTION``. For instance, :: class MyApp(cli.Application): PROGNAME = "Foobar" VERSION = "7.3" Colors ^^^^^^ .. versionadded:: 1.6 Colors are supported. You can use a colored string on ``PROGNAME``, ``VERSION`` and ``DESCRIPTION`` directly. If you set ``PROGNAME`` to a color, you can get auto-naming and color. The color of the usage string is available as ``COLOR_USAGE``. The color of ``Usage:`` line itself may be specified using ``COLOR_USAGE_TITLE``, otherwise it defaults to ``COLOR_USAGE``. Different groups can be colored with a dictionaries ``COLOR_GROUPS`` and ``COLOR_GROUP_TITLES``. For instance, the following is valid:: class MyApp(cli.Application): PROGNAME = colors.green VERSION = colors.blue | "1.0.2" COLOR_GROUPS = {"Switches": colors.blue | "Meta-switches" : colors.yellow} COLOR_GROUP_TITLES = {"Switches": colors.bold | colors.blue, "Meta-switches" : colors.bold & colors.yellow} opts = cli.Flag("--ops", help=colors.magenta | "This is help") .. raw:: html
    SimpleColorCLI.py 1.0.2

    Usage:
        SimpleColorCLI.py [SWITCHES] 

    Meta-switches
        -h, --help         Prints this help message and quits
        --help-all         Print help messages of all subcommands and quit
        -v, --version      Prints the program's version and quits
    

    Switches:
        --ops              This is help
    
Switch Functions ---------------- The decorator :func:`switch ` can be seen as the "heart and soul" of the CLI toolkit; it exposes methods of your CLI application as CLI-switches, allowing them to be invoked from the command line. Let's examine the following toy application:: class MyApp(cli.Application): _allow_root = False # provide a default @cli.switch("--log-to-file", str) def log_to_file(self, filename): """Sets the file into which logs will be emitted""" logger.addHandler(FileHandle(filename)) @cli.switch(["-r", "--root"]) def allow_as_root(self): """If given, allow running as root""" self._allow_root = True def main(self): if os.geteuid() == 0 and not self._allow_root: raise ValueError("cannot run as root") When the program is run, the switch functions are invoked with their appropriate arguments; for instance, ``$ ./myapp.py --log-to-file=/tmp/log`` would translate to a call to ``app.log_to_file("/tmp/log")``. After all switches were processed, control passes to ``main``. .. note:: Methods' docstrings and argument names will be used to render the help message, keeping your code as `DRY `_ as possible. There's also :func:`autoswitch `, which infers the name of the switch from the function's name, e.g.:: @cli.autoswitch(str) def log_to_file(self, filename): pass Will bind the switch function to ``--log-to-file``. Arguments ^^^^^^^^^ As demonstrated in the example above, switch functions may take no arguments (not counting ``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:: class MyApp(cli.Application): _port = 8080 @cli.switch(["-p"], int) def server_port(self, port): self._port = port def main(self): print(self._port) :: $ ./example.py -p 17 17 $ ./example.py -p foo Argument of -p expected to be , not 'foo': ValueError("invalid literal for int() with base 10: 'foo'",) The toolkit includes two additional "types" (or rather, *validators*): ``Range`` and ``Set``. ``Range`` takes a minimal value and a maximal value and expects an integer in 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:: class MyApp(cli.Application): _port = 8080 _mode = "TCP" @cli.switch("-p", cli.Range(1024,65535)) def server_port(self, port): self._port = port @cli.switch("-m", cli.Set("TCP", "UDP", case_sensitive = False)) def server_mode(self, mode): self._mode = mode def main(self): print(self._port, self._mode) :: $ ./example.py -p 17 Argument of -p expected to be [1024..65535], not '17': ValueError('Not in range [1024..65535]',) $ ./example.py -m foo Argument of -m expected to be Set('udp', 'tcp'), not 'foo': ValueError("Expected one of ['UDP', 'TCP']",) .. note:: The toolkit also provides some other useful validators: ``ExistingFile`` (ensures the given argument is an existing file), ``ExistingDirectory`` (ensures the given argument is an existing directory), and ``NonexistentPath`` (ensures the given argument is not an existing path). All of these convert the argument to a :ref:`local path `. 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:: class MyApp(cli.Application): _dirs = [] @cli.switch("-I", str, list = True) def include_dirs(self, dirs): self._dirs = dirs def main(self): print(self._dirs) :: $ ./example.py -I/foo/bar -I/usr/include ['/foo/bar', '/usr/include'] .. note:: The switch function will be called **only once**, and its argument will be a list of items Mandatory Switches ^^^^^^^^^^^^^^^^^^ If a certain switch is required, you can specify this by passing ``mandatory = True`` to the ``switch`` decorator. The user will not be able to run the program without specifying a value for this switch. Dependencies ^^^^^^^^^^^^ 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 will not be able to run the program. :: class MyApp(cli.Application): @cli.switch("--log-to-file", str) def log_to_file(self, filename): logger.addHandler(logging.FileHandler(filename)) @cli.switch("--verbose", requires = ["--log-to-file"]) def verbose(self): logger.setLevel(logging.DEBUG) :: $ ./example --verbose Given --verbose, the following are missing ['log-to-file'] .. warning:: The toolkit invokes the switch functions in the same order in which the switches were given on the command line. It doesn't go as far as computing a topological order on the fly, but this will change in the future. Mutual Exclusion ^^^^^^^^^^^^^^^^^ Just as some switches may depend on others, some switches mutually-exclude others. For instance, it does not make sense to allow ``--verbose`` and ``--terse``. For this purpose, you can set the ``excludes`` list in the ``switch`` decorator. :: class MyApp(cli.Application): @cli.switch("--log-to-file", str) def log_to_file(self, filename): logger.addHandler(logging.FileHandler(filename)) @cli.switch("--verbose", requires = ["--log-to-file"], excludes = ["--terse"]) def verbose(self): logger.setLevel(logging.DEBUG) @cli.switch("--terse", requires = ["--log-to-file"], excludes = ["--verbose"]) def terse(self): logger.setLevel(logging.WARNING) :: $ ./example --log-to-file=log.txt --verbose --terse Given --verbose, the following are invalid ['--terse'] Grouping ^^^^^^^^ If you wish to group certain switches together in the help message, you can specify ``group = "Group Name"``, where ``Group Name`` is any string. When the help message is rendered, all the switches that belong to the same group will be grouped together. Note that grouping has no other effects on the way switches are processed, but it can help improve the readability of the help message. 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 `, which is `data descriptor `_ 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):: class MyApp(cli.Application): log_file = cli.SwitchAttr("--log-file", str, default = None) enable_logging = cli.Flag("--no-log", default = True) verbosity_level = cli.CountOf("-v") def main(self): print(self.log_file, self.enable_logging, self.verbosity_level) .. code-block:: bash $ ./example.py -v --log-file=log.txt -v --no-log -vvv log.txt False 5 Environment Variables ^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.6 You can also set a ``SwitchAttr`` to take an environment variable as an input using the envname parameter. For example:: class MyApp(cli.Application): log_file = cli.SwitchAttr("--log-file", str, envname="MY_LOG_FILE") def main(self): print(self.log_file) .. code-block:: bash $ MY_LOG_FILE=this.log ./example.py this.log Giving the switch on the command line will override the environment variable value. Main ---- The ``main()`` method takes control once all the command-line switches have been processed. It may take any number of *positional argument*; for instance, in ``cp -r /foo /bar``, ``/foo`` and ``/bar`` are the *positional arguments*. The number of positional arguments 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:: class MyApp(cli.Application): def main(self, src, dst, mode = "normal"): print(src, dst, mode) :: $ ./example.py /foo /bar /foo /bar normal $ ./example.py /foo /bar spam /foo /bar spam $ ./example.py /foo Expected at least 2 positional arguments, got ['/foo'] $ ./example.py /foo /bar spam bacon Expected at most 3 positional arguments, got ['/foo', '/bar', 'spam', 'bacon'] .. note:: The method's signature is also used to generate the help message, e.g. :: Usage: [SWITCHES] src dst [mode='normal'] With varargs:: class MyApp(cli.Application): def main(self, src, dst, *eggs): print(src, dst, eggs) :: $ ./example.py a b c d a b ('c', 'd') $ ./example.py --help Usage: [SWITCHES] src dst eggs... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Positional argument validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.6 You can supply positional argument validators using the ``cli.positional`` decorator. Simply pass the validators in the decorator matching the names in the main function. For example:: class MyApp(cli.Application): @cli.positional(cli.ExistingFile, cli.NonexistentPath) def main(self, infile, *outfiles): "infile is a path, outfiles are a list of paths, proper errors are given" You can also use annotations to specify the validators. For example:: class MyApp(cli.Application): def main(self, infile : cli.ExistingFile, *outfiles : cli.NonexistentPath): "Identical to above MyApp" Annotations are ignored if the positional decorator is present. Switch Abbreviations ^^^^^^^^^^^^^^^^^^^^ The cli supports switches which have been abbreviated by the user, for example, "--h", "--he", or "--hel" would all match an actual switch name of"--help", as long as no ambiguity arises from multiple switches that might match the same abbreviation. This behavior is disabled by default but can be enabled by defining the class-level attribute ``ALLOW_ABBREV`` to True. For example:: class MyApp(cli.Application): ALLOW_ABBREV = True cheese = cli.Flag(["cheese"], help = "cheese, please") chives = cli.Flag(["chives"], help = "chives, instead") With the above definition, running the following will raise an error due to ambiguity:: $ python3 example.py --ch # error! matches --cheese and --chives However, the following two lines are equivalent:: $ python3 example.py --che $ python3 example.py --cheese .. _guide-subcommands: Sub-commands ------------ .. versionadded:: 1.1 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 `_, 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. Before we get to the code, it is important to stress out two things: * Under Plumbum, each sub-command is a full-fledged ``cli.Application`` on its own; if you wish, you can execute it separately, detached from its so-called root application. When an application is run independently, its ``parent`` attribute is ``None``; when it is run as a sub-command, its ``parent`` attribute points to its parent application. Likewise, when an parent application is executed with a sub-command, its ``nested_command`` is set to the nested application; otherwise it's ``None``. * Each sub-command is responsible of **all** arguments that follow it (up to the next sub-command). This allows applications to process their own switches and positional arguments before the nested application is invoked. Take, for instance, ``git --foo=bar spam push origin --tags``: the root application, ``git``, is in charge of the switch ``--foo`` and the positional argument ``spam``, and the nested application ``push`` is in charge of the arguments that follow it. In theory, you can nest several sub-applications one into the other; in practice, only a single level 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 attached to the root application using the ``subcommand`` decorator :: class Geet(cli.Application): """The l33t version control""" VERSION = "1.7.2" def main(self, *args): if args: print(f"Unknown command {args[0]}") return 1 # error exit code if not self.nested_command: # will be ``None`` if no sub-command follows print("No command given") return 1 # error exit code @Geet.subcommand("commit") # attach 'geet commit' class GeetCommit(cli.Application): """creates a new commit in the current branch""" auto_add = cli.Flag("-a", help = "automatically add changed files") message = cli.SwitchAttr("-m", str, mandatory = True, help = "sets the commit message") def main(self): print("doing the commit...") @Geet.subcommand("push") # attach 'geet push' class GeetPush(cli.Application): """pushes the current local branch to the remote one""" def main(self, remote, branch = None): print("doing the push...") if __name__ == "__main__": Geet.run() .. note:: * Since ``GeetCommit`` is a ``cli.Application`` on its own right, you may invoke ``GeetCommit.run()`` directly (should that make sense in the context of your application) * You can also attach sub-commands "imperatively", using ``subcommand`` as a method instead of a decorator: ``Geet.subcommand("push", GeetPush)`` Here's an example of running this application:: $ python3 geet.py --help geet v1.7.2 The l33t version control Usage: geet.py [SWITCHES] [SUBCOMMAND [SWITCHES]] args... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Subcommands: commit creates a new commit in the current branch; see 'geet commit --help' for more info push pushes the current local branch to the remote one; see 'geet push --help' for more info $ python3 geet.py commit --help geet commit v1.7.2 creates a new commit in the current branch Usage: geet commit [SWITCHES] Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Switches: -a automatically add changed files -m VALUE:str sets the commit message; required $ python3 geet.py commit -m "foo" committing... Configuration parser -------------------- Another common task of a cli application is provided by a configuration parser, with an INI backend: ``Config`` (or ``ConfigINI`` to explicitly request the INI backend). An example of it's use:: from plumbum import cli with cli.Config('~/.myapp_rc') as conf: one = conf.get('one', '1') two = conf.get('two', '2') If no configuration file is present, this will create one and each call to ``.get`` will set the value with the given default. The file is created when the context manager exits. If the file is present, it is read and the values from the file are selected, and nothing is changed. You can also use ``[]`` syntax to forcibly set a value, or to get a value with a standard ``ValueError`` if not present. If you want to avoid the context manager, you can use ``.read`` and ``.write`` as well. The ini parser will default to using the ``[DEFAULT]`` section for values, just like Python's ConfigParser on which it is based. If you want to use a different section, simply separate section and heading with a ``.`` in the key. ``conf['section.item']`` would place ``item`` under ``[section]``. All items stored in an ``ConfigINI`` are converted to ``str``, and ``str`` is always returned. Terminal Utilities ------------------ Several terminal utilities are available in ``plumbum.cli.terminal`` to assist in making terminal applications. ``get_terminal_size(default=(80,25))`` allows cross platform access to the terminal size as a tuple ``(width, height)``. Several methods to ask the user for input, such as ``readline``, ``ask``, ``choose``, and ``prompt`` are available. ``Progress(iterator)`` allows you to quickly create a progress bar from an iterator. Simply wrap a slow iterator with this and iterate over it, and it will produce a nice text progress bar based on the user's screen width, with estimated time remaining displayed. If you need to create a progress bar for a fast iterator but with a loop containing code, use ``Progress.wrap`` or ``Progress.range``. For example:: for i in Progress.range(10): time.sleep(1) If you have something that produces output, but still needs a progress bar, pass ``has_output=True`` to force the bar not to try to erase the old one each time. A command line image plotter (``Image``) is provided in ``plumbum.cli.image``. It can plot a PIL-like image ``im`` using:: Image().show_pil(im) The Image constructor can take an optional size (defaults to the current terminal size if None), and a ``char_ratio``, a height to width measure for your current font. It defaults to a common value of 2.45. If set to None, the ratio is ignored and the image will no longer be constrained to scale proportionately. To directly plot an image, the ``show`` method takes a filename and a double parameter, which doubles the vertical resolution on some fonts. The ``show_pil`` and ``show_pil_double`` methods directly take a PIL-like object. To plot an image from the command line, the module can be run directly: ``python3 -m plumbum.cli.image myimage.png``. For the full list of helpers or more information, see the :ref:`api docs `. See Also -------- * `filecopy.py `_ example * `geet.py `_ – a runnable example of using sub-commands * `RPyC `_ has changed its bash-based build script to Plumbum CLI. Notice `how short and readable `_ it is. * A `blog post `_ describing the philosophy of the CLI module plumbum-1.8.3/docs/colorlib.rst0000644000000000000000000003252614613634536013444 0ustar00.. _guide-colorlib: Colorlib design --------------- .. versionadded:: 1.6 The purpose of this document is to describe the system that plumbum.colors implements. This system was designed to be flexible and to allow implementing new color backends. Hopefully this document will allow future work on colorlib to be as simple as possible. .. note:: Enabling color ``plumbum.colors`` tries to guess the color output settings of your system. You can force the use of color globally by setting ``colors.use_color=True`` See :ref:`guide-colorlist` for more options. Generating colors ================= Styles are accessed through the ``colors`` object, which is an instance of a StyleFactory. The ``colors`` object is actually an imitation module that wraps ``plumbum.colorlib.ansicolors`` with module-like access. Thus, things like from ``plumbum.colors.bg import red`` work also. The library actually lives in ``plumbum.colorlib``. Style Factory ^^^^^^^^^^^^^ The ``colors`` object has the following available objects: ``fg`` and ``bg`` The foreground and background colors, reset to default with ``colors.fg.reset`` or ``~colors.fg`` and likewise for ``bg``. These are ``ColorFactory`` instances. ``bold``, ``dim``, ``underline``, ``italics``, ``reverse``, ``strikeout``, and ``hidden`` All the `ANSI` modifiers are available, as well as their negations, such as ``~colors.bold`` or ``colors.bold.reset``, etc. (These are generated automatically based on the Style attached to the factory.) ``reset`` The global reset will restore all properties at once. ``do_nothing`` Does nothing at all, but otherwise acts like any ``Style`` object. It is its own inverse. Useful for ``cli`` properties. The ``colors`` object can be used in a with statement, which resets all styles on leaving the statement body. Although factories do support some of the same methods as a Style, their primary purpose is to generate Styles. The colors object has a ``use_color`` property that can be set to force the use of color. A ``stdout`` property is provided to make changing the output of color statement easier. A ``colors.from_ansi(code)`` method allows you to create a Style from any ansi sequence, even complex or combined ones. Color Factories ^^^^^^^^^^^^^^^ The ``colors.fg`` and ``colors.bg`` are ``ColorFactory``'s. In fact, the colors object itself acts exactly like the ``colors.fg`` object, with the exception of the properties listed above. Named foreground colors are available directly as methods. The first 16 primary colors, ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, etc, as well as ``reset``, are available. All 256 color names are available, but do not populate factory directly, so that auto-completion gives reasonable results. You can also access colors using strings and do ``colors[string]``. Capitalization, underscores, and spaces (for strings) will be ignored. You can also access colors numerically with ``colors(n)`` or ``colors[n]`` with the extended 256 color codes. The former will default to simple versions of colors for the first 16 values. The later notation can also be used to slice. Full hex codes can be used, too. If no match is found, these will be the true 24 bit color value. The ``fg`` and ``bg`` also can be put in with statements, and they will restore the foreground and background color only, respectively. ``colors.rgb(r,g,b)`` will create a color from an input red, green, and blue values (integers from 0-255). ``colors.rgb(code)`` will allow you to input an html style hex sequence. These work on ``fg`` and ``bg`` too. The ``repr`` of styles is smart and will show you the closest color to the one you selected if you didn't exactly select a color through RGB. Style manipulations =================== Safe color manipulations refer to changes that reset themselves at some point. Unsafe manipulations must be manually reset, and can leave your terminal color in an unreadable state if you forget to reset the color or encounter an exception. If you do get the color unset on a terminal, the following, typed into the command line, will restore it: .. code:: bash $ python3 -m plumbum.colors This also supports command line access to unsafe color manipulations, such as .. code:: bash $ python3 -m plumbum.colors blue $ python3 -m plumbum.colors bg red $ python3 -m plumbum.colors fg 123 $ python3 -m plumbum.colors bg reset $ python3 -m plumbum.colors underline You can use any path or number available as a style. Unsafe Manipulation ^^^^^^^^^^^^^^^^^^^ Styles have two unsafe operations: Concatenation (with ``+`` and a string) and calling ``.now()`` without arguments (directly calling a style without arguments is also a shortcut for ``.now()``). These two operations do not restore normal color to the terminal by themselves. To protect their use, you should always use a context manager around any unsafe operation. An example of the usage of unsafe ``colors`` manipulations inside a context manager:: from plumbum import colors with colors: colors.fg.red.now() print('This is in red') colors.green.now() print('This is green ' + colors.underline + 'and now also underlined!') print('Underlined' + colors.underline.reset + ' and not underlined but still red') print('This is completely restored, even if an exception is thrown!') Output: .. raw:: html

This is in red
This is in green and now also underlined!
Underlined and not underlined but still green.
This is completely restored, even if an exception is thrown!

We can use ``colors`` instead of ``colors.fg`` for foreground colors. If we had used ``colors.fg`` as the context manager, then non-foreground properties, such as ``colors.underline`` or ``colors.bg.yellow``, would not have reset those properties. Each attribute, as well as ``fg``, ``bg``, and ``colors`` all have inverses in the ANSI standard. They are accessed with ``~`` or ``.reset``, and can be used to manually make these operations safer, but there is a better way. Safe Manipulation ^^^^^^^^^^^^^^^^^ All other operations are safe; they restore the color automatically. The first, and hopefully already obvious one, is using a Style rather than a ``colors`` or ``colors.fg`` object in a ``with`` statement. This will set the color (using sys.stdout by default) to that color, and restore color on leaving. The second method is to manually wrap a string. This can be done with ``color.wrap("string")`` or ``color["string"]``. These produce strings that can be further manipulated or printed. Finally, you can also print a color to stdout directly using ``color.print("string")``. This has the same syntax as the print function. An example of safe manipulations:: colors.fg.yellow('This is yellow', end='') print(' And this is normal again.') with colors.red: print('Red color!') with colors.bold: print("This is red and bold.") print("Not bold, but still red.") print("Not red color or bold.") print((colors.magenta & colors.bold)["This is bold and colorful!"], "And this is not.") Output: .. raw:: html

This is yellow And this is normal again.
Red color!
This is red and bold.
Not bold, but still red.
Not red color or bold.
This is bold and colorful! And this is not.

Style Combinations ^^^^^^^^^^^^^^^^^^ You can combine styles with ``&`` and they will create a new combined Style object. Colors will not be "summed" or otherwise combined; the rightmost color will be used (this matches the expected effect of applying the Styles individually to the strings). However, combined Styles are intelligent and know how to reset just the properties that they contain. As you have seen in the example above, the combined style ``(colors.magenta & colors.bold)`` can be used in any way a normal Style can. Since wrapping is done with ``|``, the Python order of operations causes styles to be combined first, then wrapping is done last. .. _guide-colorlist: 256 Color Support ================= While this library supports full 24 bit colors through escape sequences, the library has special support for the "full" 256 colorset through numbers, names or HEX html codes. Even if you use 24 bit color, the closest name is displayed in the ``repr``. You can access the colors as as ``colors.fg.Light_Blue``, ``colors.fg.lightblue``, ``colors.fg[12]``, ``colors.fg('Light_Blue')``, ``colors.fg('LightBlue')``, or ``colors.fg('#0000FF')``. You can also iterate or slice the ``colors``, ``colors.fg``, or ``colors.bg`` objects. Slicing even intelligently downgrades to the simple version of the codes if it is within the first 16 elements. The supported colors are: .. raw:: html :file: _color_list.html If you want to enforce a specific representation, you can use ``.basic`` (8 color), ``.simple`` (16 color), ``.full`` (256 color), or ``.true`` (24 bit color) on a Style, and the colors in that Style will conform to the output representation and name of the best match color. The internal RGB colors are remembered, so this is a non-destructive operation. To limit the use of color to one of these styles, set ``colors.use_color`` to 1 for 8 colors, 2 for 16 colors, 3 for 256 colors, or 4 for true color. It will be guessed based on your system on initialisation. The Classes =========== The library consists of three primary classes, the ``Color`` class, the ``Style`` class, and the ``StyleFactory`` class. The following portion of this document is primarily dealing with the working of the system, and is meant to facilitate extensions or work on the system. The ``Color`` class provides meaning to the concept of color, and can provide a variety of representations for any color. It can be initialised from r,g,b values, or hex codes, 256 color names, or the simple color names via classmethods. If initialized without arguments, it is the reset color. It also takes an fg True/False argument to indicate which color it is. You probably will not be interacting with the Color class directly, and you probably will not need to subclass it, though new extensions to the representations it can produce are welcome. The ``Style`` class hold two colors and a dictionary of attributes. It is the workhorse of the system and is what is produced by the ``colors`` factory. It holds ``Color`` as ``.color_class``, which can be overridden by subclasses (again, this usually is not needed). To create a color representation, you need to subclass ``Style`` and give it a working ``__str__`` definition. ``ANSIStyle`` is derived from ``Style`` in this way. The factories, ``ColorFactory`` and ``StyleFactory``, are factory classes that are meant to provide simple access to 1 style Style classes. To use, you need to initialize an object of ``StyleFactory`` with your intended Style. For example, ``colors`` is created by:: colors = StyleFactory(ANSIStyle) Subclassing Style ^^^^^^^^^^^^^^^^^ For example, if you wanted to create an HTMLStyle and HTMLcolors, you could do:: class HTMLStyle(Style): attribute_names = dict(bold='b', li='li', code='code') end = '
\n' def __str__(self): result = '' if self.bg and not self.bg.reset: result += f'' if self.fg and not self.fg.reset: result += f'' for attr in sorted(self.attributes): if self.attributes[attr]: result += '<' + self.attribute_names[attr] + '>' for attr in reversed(sorted(self.attributes)): if not self.attributes[attr]: result += '' if self.fg and self.fg.reset: result += '' if self.bg and self.bg.reset: result += '' return result htmlcolors = StyleFactory(HTMLStyle) This doesn't support global resets, since that's not how HTML works, but otherwise is a working implementation. This is an example of how easy it is to add support for other output formats. An example of usage:: >>> htmlcolors.bold & htmlcolors.red | "This is colored text" 'This is colored text' The above color table can be generated with:: for color in htmlcolors: htmlcolors.li( "■" | color, color.fg.hex_code | htmlcolors.code, color.fg.name_camelcase) .. note:: ``HTMLStyle`` is implemented in the library, as well, with the ``htmlcolors`` object available in ``plumbum.colorlib``. It was used to create the colored output in this document, with small changes because ``colors.reset`` cannot be supported with HTML. See Also ======== * `colored `_ Another library with 256 color support * `colorama `_ A library that supports colored text on Windows, can be combined with Plumbum.colors (if you force ``use_color``, doesn't support all extended colors) plumbum-1.8.3/docs/colors.rst0000644000000000000000000002677214613634536013146 0ustar00.. _guide-colors: Colors ------ .. versionadded:: 1.6 The purpose of the ``plumbum.colors`` library is to make adding text styles (such as color) to Python easy and safe. Color is often a great addition to shell scripts, but not a necessity, and implementing it properly is tricky. It is easy to end up with an unreadable color stuck on your terminal or with random unreadable symbols around your text. With the color module, you get quick, safe access to ANSI colors and attributes for your scripts. The module also provides an API for creating other color schemes for other systems using escapes. .. note:: Enabling color ``ANSIStyle`` assumes that only a terminal can display color, and looks at the value of the environment variable ``TERM``. You can force the use of color globally by setting ``colors.use_color=4`` (The levels 0-4 are available, with 0 being off). See this :ref:`note ` for more options. Quick start =========== Colors (``red``, ``green``, etc.), attributes (``bold``, ``underline``, etc.) and general styles (``warn``, ``info``, etc.) are in ``plumbum.colors``. Combine styles with ``&``, apply to a string with ``|``. So, to output a warning you would do .. code-block:: python from plumbum.colors import warn print(warn | "This is a warning.") .. raw:: html

This is a warning. To create a custom style you would do .. code-block:: python from plumbum import colors print(colors.green & colors.bold | "This is green and bold.") .. raw:: html This is green and bold. You can use rgb colors, too: .. code-block:: python print(colors.rgb(0,255,0) | "This is also green.") .. raw:: html This is also green Generating Styles ================= Styles are accessed through the ``plumbum.colors`` object. This has the following available objects: ``fg`` and ``bg`` The foreground and background colors, reset to default with ``colors.fg.reset`` or ``~colors.fg`` and likewise for ``bg``. ``bold``, ``dim``, ``underline``, ``italics``, ``reverse``, ``strikeout``, and ``hidden`` All the `ANSI` modifiers are available, as well as their negations, such as ``~colors.bold`` or ``colors.bold.reset``, etc. ``reset`` The global reset will restore all properties at once. ``do_nothing`` Does nothing at all, but otherwise acts like any ``Style`` object. It is its own inverse. Useful for ``cli`` properties. Styles loaded from a stylesheet dictionary, such as ``warn`` and ``info``. These allow you to set standard styles based on behavior rather than colors, and you can load a new stylesheet with ``colors.load_stylesheet(...)``. Recreating and loading the default stylesheet would look like this: .. code-block:: python >>> default_styles = dict( ... warn="fg red", ... title="fg cyan underline bold", ... fatal="fg red bold", ... highlight="bg yellow", ... info="fg blue", ... success="fg green") >>> colors.load_stylesheet(default_styles) The ``colors.from_ansi(code)`` method allows you to create a Style from any ansi sequence, even complex or combined ones. Colors ^^^^^^ The ``colors.fg`` and ``colors.bg`` allow you to access and generate colors. Named foreground colors are available directly as methods. The first 16 primary colors, ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, etc, as well as ``reset``, are available. All 256 color names are available, but do not populate directly, so that auto-completion gives reasonable results. You can also access colors using strings and do ``colors.fg[string]``. Capitalization, underscores, and spaces (for strings) will be ignored. You can also access colors numerically with ``colors.fg[n]`` for the extended 256 color codes. ``colors.fg.rgb(r,g,b)`` will create a color from an input red, green, and blue values (integers from 0-255). ``colors.fg.rgb(code)`` will allow you to input an html style hex sequence. Anything you can access from ``colors.fg`` can also be accessed directly from ``colors``. 256 Color Support ================= While this library supports full 24 bit colors through escape sequences, the library has special support for the "full" 256 colorset through numbers, names or HEX html codes. Even if you use 24 bit color, the closest name is displayed in the ``repr``. You can access the colors as as ``colors.fg.Light_Blue``, ``colors.fg.lightblue``, ``colors.fg[12]``, ``colors.fg('Light_Blue')``, ``colors.fg('LightBlue')``, or ``colors.fg('#0000FF')``. You can also iterate or slice the ``colors``, ``colors.fg``, or ``colors.bg`` objects. Slicing even intelligently downgrades to the simple version of the codes if it is within the first 16 elements. The supported colors are: .. raw:: html :file: _color_list.html If you want to enforce a specific representation, you can use ``.basic`` (8 color), ``.simple`` (16 color), ``.full`` (256 color), or ``.true`` (24 bit color) on a style, and the colors in that Style will conform to the output representation and name of the best match color. The internal RGB colors are remembered, so this is a non-destructive operation. .. _guide-usecolors: .. note:: Some terminals only support a subset of colors, so keep this in mind when using a larger color set. The standard Ubuntu terminal handles 24 bit color, the Mac terminal only handles 256 colors, and Colorama on Windows only handles 8. See `this gist `_ for information about support in terminals. If you need to limit the output color, you can set ``colors.use_color`` to 0 (no colors), 1 (8 colors), 2 (16 colors), or 3 (256 colors), or 4 (24-bit colors). This option will be automatically guessed for you on initialization. Style manipulations =================== Safe color manipulations refer to changes that reset themselves at some point. Unsafe manipulations must be manually reset, and can leave your terminal color in an unreadable state if you forget to reset the color or encounter an exception. The library is smart and will try to restore the color when Python exits. .. note:: If you do get the color unset on a terminal, the following, typed into the command line, will restore it: .. code:: bash $ python3 -m plumbum.colors This also supports command line access to unsafe color manipulations, such as .. code:: bash $ python3 -m plumbum.colors blue $ python3 -m plumbum.colors bg red $ python3 -m plumbum.colors fg 123 $ python3 -m plumbum.colors bg reset $ python3 -m plumbum.colors underline You can use any path or number available as a style. Unsafe Manipulation ^^^^^^^^^^^^^^^^^^^ Styles have two unsafe operations: Concatenation (with ``+`` and a string) and calling ``.now()`` without arguments (directly calling a style without arguments is also a shortcut for ``.now()``). These two operations do not restore normal color to the terminal by themselves. To protect their use, you can use a context manager around any unsafe operation. An example of the usage of unsafe ``colors`` manipulations inside a context manager:: from plumbum import colors with colors: colors.fg.red.now() print('This is in red') .. raw:: html

This is in red
This is in green and now also underlined!
Underlined and not underlined but still green.
This is completely restored, even if an exception is thrown!

colors.green.now() print('This is green ' + colors.underline + 'and now also underlined!') print('Underlined' + colors.underline.reset + ' and not underlined but still red') print('This is completely restored, even if an exception is thrown!') Output: .. raw:: html

This is in red
This is in green and now also underlined!
Underlined and not underlined but still green.
This is completely restored, even if an exception is thrown!

We can use ``colors`` instead of ``colors.fg`` for foreground colors. If we had used ``colors.fg`` as the context manager, then non-foreground properties, such as ``colors.underline`` or ``colors.bg.yellow``, would not have been reset. Each attribute, as well as ``fg``, ``bg``, and ``colors`` all have inverses in the ANSI standard. They are accessed with ``~`` or ``.reset``, and can be used to manually make these operations safer, but there is a better way. Safe Manipulation ^^^^^^^^^^^^^^^^^ All other operations are safe; they restore the color automatically. The first, and hopefully already obvious one, is using a specific style rather than a ``colors`` or ``colors.fg`` object in a ``with`` statement. This will set the color (using ``sys.stdout`` by default) to that color, and restore color on leaving. The second method is to manually wrap a string. This can be done with ``color | "string"`` or ``color["string"]``. These produce strings that can be further manipulated or printed. Finally, you can also print a color to stdout directly using ``color.print("string")``. This has the same syntax as the print function. An example of safe manipulations:: colors.fg.yellow('This is yellow', end='') print(' And this is normal again.') with colors.red: print('Red color!') with colors.bold: print("This is red and bold.") print("Not bold, but still red.") print("Not red color or bold.") print(colors.magenta & colors.bold | "This is bold and colorful!", "And this is not.") Output: .. raw:: html

This is yellow And this is normal again.
Red color!
This is red and bold.
Not bold, but still red.
Not red color or bold.
This is bold and colorful! And this is not.

Style Combinations ^^^^^^^^^^^^^^^^^^ You can combine styles with ``&`` and they will create a new combined style. Colors will not be "summed" or otherwise combined; the rightmost color will be used (this matches the expected effect of applying the styles individually to the strings). However, combined styles are intelligent and know how to reset just the properties that they contain. As you have seen in the example above, the combined style ``(colors.magenta & colors.bold)`` can be used in any way a normal style can. New color systems ================= The library was written primarily for ANSI color sequences, but can also easily be subclassed to create new color systems. See :ref:`guide-colorlib` for information on how the system works. An HTML version is available as ``plumbum.colorlib.htmlcolors``. See Also ======== * `colored `_ Another library with 256 color support * `colorful `_ A fairly new library with a similar feature set * `colorama `_ A library that supports colored text on Windows, can be combined with Plumbum.colors (if you force ``use_color``, doesn't support all extended colors) * `rich `_ A very powerful modern library for all sorts of styling. plumbum-1.8.3/docs/conf.py0000644000000000000000000002027414613634536012401 0ustar00# # Plumbum Shell Combinators documentation build configuration file, created by # sphinx-quickstart on Sun Apr 29 16:24:32 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import time # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Plumbum Shell Combinators" copyright = "%d, Tomer Filiba, licensed under MIT" % (time.gmtime().tm_year,) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version is release. from plumbum.version import version as release from plumbum.version import version_tuple version = ".".join(str(v) for v in version_tuple[:2]) autodoc_member_order = "bysource" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build", "_news.rst", "_cheatsheet.rst"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'default' html_theme = "haiku" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {"full_logo": True} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "Plumbum: Shell Combinators" # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = "" # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/logo8.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "PlumbumShellCombinatorsdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "PlumbumShellCombinators.tex", "Plumbum Shell Combinators Documentation", "Tomer Filiba", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "plumbumshellcombinators", "Plumbum Shell Combinators Documentation", ["Tomer Filiba"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "PlumbumShellCombinators", "Plumbum Shell Combinators Documentation", "Tomer Filiba", "PlumbumShellCombinators", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' plumbum-1.8.3/docs/index.rst0000644000000000000000000001753714613634536012753 0ustar00.. raw:: html Plumbum: Shell Combinators and More =================================== .. comment raw:: html
Sticky
Version 3.2.3 was released on December 2nd
Please use the mailing list to ask questions and use github issues to report problems. Please do not email me directly.
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 :ref:`shell-like syntax ` and :ref:`handy shortcuts `, the library provides local and :ref:`remote ` command execution (over SSH), local and remote file-system :ref:`paths `, easy working-directory and environment :ref:`manipulation `, quick access to ANSI :ref:`colors `, and a programmatic :ref:`guide-cli` application toolkit. Now let's see some code! News ==== .. include:: _news.rst * :doc:`changelog` * :doc:`quickref` Cheat Sheet =========== .. include:: _cheatsheet.rst Development and Installation ============================ The library is developed on `GitHub `_, and will happily accept `patches `_ from users. Please use the GitHub's built-in `issue tracker `_ to report any problem you encounter or to request features. The library is released under the permissive `MIT license `_. Requirements ------------ Plumbum supports **Python 3.6-3.10** and **PyPy** and is continually tested on **Linux**, **Mac**, and **Windows** machines through `GitHub Actions `_. Any Unix-like machine should work fine out of the box, but on Windows, you'll probably want to install a decent `coreutils `_ environment and add it to your ``PATH``, or use WSL(2). I can recommend `mingw `_ (which comes bundled with `Git for Windows `_), but `cygwin `_ 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. Note that for remote command execution, an **openSSH-compatible** client is required (also bundled with *Git for Windows*), and a ``bash``-compatible shell and a coreutils environment is also expected on the host machine. This project uses ``setuptools`` to build wheels; and ``setuptools_scm`` is required for building SDists. These dependencies will be handled for you by PEP 518 compatible builders, like `build `_ and ``pip 10+``. Download -------- You can **download** the library from the `Python Package Index `_ (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``. User Guide ========== The user guide covers most of the features of Plumbum, with lots of code-snippets to get you swimming in no time. It introduces the concepts and "syntax" gradually, so it's recommended you read it in order. A quick :ref:`reference guide is available `. .. toctree:: :maxdepth: 2 local_commands paths local_machine remote utils cli typed_env colors changelog quickref API Reference ============= The API reference (generated from the *docstrings* within the library) covers all of the exposed APIs of the library. Note that some "advanced" features and some function parameters are missing from the guide, so you might want to consult with the API reference in these cases. .. toctree:: :maxdepth: 2 api/cli api/commands api/machines api/path api/fs api/colors colorlib .. note:: The ``local`` object is an instance of a ``machine``. About ===== The original purpose of Plumbum was to enable local and remote program execution with ease, assuming nothing fancier than good-old SSH. On top of this, a file-system abstraction layer was devised, so that working with local and remote files would be seamless. I've toyed with this idea for some time now, but it wasn't until I had to write build scripts 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 `_. 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 `_) of `Andrew Moffat `_, 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, and that the syntax wasn't what I had in mind when I came to write shell-like programs. I contacted Andrew about these issues, but he wanted to keep PBS this way. Other than that, the two libraries go in different directions, where Plumbum attempts to provide a more wholesome approach. Plumbum also pays tribute to `Rotem Yaari `_ who suggested a library code-named ``pyplatform`` for that very purpose, but which had never materialized. plumbum-1.8.3/docs/local_commands.rst0000644000000000000000000003241314613634536014605 0ustar00.. _guide-local-commands: Local Commands ============== Plumbum exposes a special singleton object named ``local``, which represents your local machine and serves as a factory for command objects:: >>> from plumbum import local >>> >>> ls = local["ls"] >>> ls >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad If you don't specify a full path, the program is searched for in your system's ``PATH`` (and if no match is found, a ``CommandNotFound`` exception is raised). Otherwise, the full path is used as given. Once you have a ``Command`` object, you can execute it like a normal function:: >>> ls() 'README.rst\nplumbum\nsetup.py\ntests\ntodo.txt\n' >>> ls("-a") '.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\n[...]' For convenience with the common case, you can use the ``.cmd`` magic property instead of the subscription syntax: >>> ls = local.cmd.ls >>> ls .. versionadded:: 1.7 The ``.cmd`` commands provider object .. _fallbacks: If you use the ``.get()`` method instead of ``[]``, you can include fallbacks to try if the first command does not exist on the machine. This can be used to get one of several equivalent commands, or it can be used to check for common locations of a command if not in the path. For example:: pandoc = local.get('pandoc', '~/AppData/Local/Pandoc/pandoc.exe', '/Program Files/Pandoc/pandoc.exe', '/Program Files (x86)/Pandoc/pandoc.exe') An exception is still raised if none of the commands are found. Unlike ``[]`` access, an exception will be raised if the executable does not exist. .. versionadded:: 1.6 The ``.get`` method .. _import-hack: With just a touch of magic, you can *import* commands from the mock module ``cmd``, like so:: >>> from plumbum.cmd import grep, cat >>> cat .. note:: There's no real module named ``plumbum.cmd``; it's a dynamically-created "module", injected into ``sys.modules`` to enable the use of ``from plumbum.cmd import foo``. As of version 1.1, you can actually ``import plumbum.cmd``, for consistency, but it's not recommended. It is important to stress that ``from plumbum.cmd import foo`` translates to ``local["foo"]`` behind the scenes. If underscores (``_``) appear in the name, and the name cannot be found in the path as-is, the underscores will be replaced by hyphens (``-``) and the name will be looked up again. This allows you to import ``apt_get`` for ``apt-get``. .. _guide-local-commands-pipelining: Pipelining ---------- In order to form pipelines and other chains, we must first learn to *bind arguments* to commands. As you've seen, *invoking* a command runs the program; by using square brackets (``__getitem__``), we can create bound commands:: >>> ls["-l"] BoundCommand(, ('-l',)) >>> grep["-v", ".py"] BoundCommand(, ('-v', '.py')) You can think of bound commands as commands that "remember" their arguments. Creating a bound command does not run the program; in order to run it, you'll need to call (invoke) it, like so: ``ls["-l"]()`` (in fact, ``ls["-l"]()`` is equivalent to ``ls("-l")``). Now that we can bind arguments to commands, forming pipelines is easy and straight-forwards, using ``|`` (bitwise-or):: >>> chain = ls["-l"] | grep[".py"] >>> print(chain) C:\Program Files\Git\bin\ls.exe -l | C:\Program Files\Git\bin\grep.exe .py >>> >>> 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 ------------------------ We can also use redirection into files (or any object that exposes a real ``fileno()``). If a string is given, it is assumed to be a file name, and a file with that name is opened for you. In this example, we're reading from ``stdin`` into ``grep world``, and redirecting the output to a file named ``tmp.txt``:: >>> import sys >>> ((grep["world"] < sys.stdin) > "tmp.txt")() hello hello world what has the world become? foo # Ctrl+D pressed '' .. note:: Parentheses are required here! ``grep["world"] < sys.stdin > "tmp.txt"`` would be evaluated according to the `rules for chained comparison operators `_ and result an exception. Right after ``foo``, Ctrl+D was pressed, which caused ``grep`` to finish. The empty string at the end is the command's ``stdout`` (and it's empty because it actually went to a file). Lo and behold, the file was created:: >>> cat("tmp.txt") 'hello world\nwhat has the world become?\n' If you need to send input into a program (through its ``stdin``), instead of writing the data to a file and redirecting this file into ``stdin``, you can use the shortcut ``<<`` (shift-left):: >>> (cat << "hello world\nfoo\nbar\spam" | grep["oo"]) () 'foo\n' Exit Codes ---------- If the command we're running fails (returns a non-zero exit code), we'll get an exception:: >>> cat("non/existing.file") Traceback (most recent call last): [...] ProcessExecutionError: Unexpected exit code: 1 Command line: | /bin/cat non/existing.file Stderr: | /bin/cat: non/existing.file: No such file or directory In order to avoid such exceptions, or when a different exit code is expected, just pass ``retcode = xxx`` as a keyword argument. If ``retcode`` is ``None``, no exception checking is performed (any exit code is accepted); otherwise, the exit code is expected to match the one you passed:: >>> cat("non/existing.file", retcode = None) '' >>> cat("non/existing.file", retcode = 17) Traceback (most recent call last): [...] ProcessExecutionError: Unexpected exit code: 1 Command line: | /bin/cat non/existing.file Stderr: | /bin/cat: non/existing.file: No such file or directory .. note:: If you wish to accept several valid exit codes, ``retcode`` may be a tuple or a list. For instance, ``grep("foo", "myfile.txt", retcode = (0, 2))`` If you need to have both the output/error and the exit code (using exceptions would provide either but not both), you can use the ``run`` method, which will provide all of them >>> cat["non/existing.file"].run(retcode=None) (1, '', '/bin/cat: non/existing.file: No such file or directory\n') If you need the value of the exit code, there are two ways to do it. You can call ``.run(retcode=None)`` (or any other valid retcode value) on a command, you will get a tuple ``(retcode, stdout, stderr)`` (see `Run and Popen`_. If you just need the retcode, or want to check the retcode, there are two special objects that can be applied to your command to run it and get or test the retcode. For example:: >>> cat["non/existing.file"] & RETCODE 1 >>> cat["non/existing.file"] & TF False >>> cat["non/existing.file"] & TF(1) True .. note:: If you want to run these commands in the foreground (see `Background and Foreground`_), you can give ``FG=True`` to ``TF`` or ``RETCODE``. For instance, ``cat["non/existing.file"] & TF(1,FG=True)`` .. versionadded:: 1.5 The ``TF`` and ``RETCODE`` modifiers Run and Popen ------------- Notice that calling commands (or chained-commands) only returns their ``stdout``. In order to get hold of the exit code or ``stderr``, you'll need to use the :func:`run ` method, which returns a 3-tuple of the exit code, ``stdout``, and ``stderr``:: >>> ls.run("-a") (0, '.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\nplumbum\[...]', '') You can also pass ``retcode`` as a keyword argument to ``run`` in the same way discussed above. And, if you want to want to execute commands "in the background" (i.e., not wait for them to finish), you can use the :func:`popen ` method, which returns a normal ``subprocess.Popen`` object:: >>> p = ls.popen("-a") >>> p.communicate() ('.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\nplumbum\n[...]', '') You can read from its ``stdout``, ``wait()`` for it, ``terminate()`` it, etc. .. _guide-local-commands-bgfg: Background and Foreground ------------------------- In order to make programming easier, there are two special objects called ``FG`` and ``BG``, which are there to help you. ``FG`` runs programs in the foreground (they receive the parent's ``stdin``, ``stdout`` and ``stderr``), and ``BG`` runs programs in the background (much like ``popen`` above, but it returns a :class:`Future ` object, instead of a ``subprocess.Popen`` one). ``FG`` is especially useful for interactive programs like editors, etc., that require a ``TTY`` or input from the user. :: >>> from plumbum import FG, BG >>> ls["-l"] & FG total 5 -rw-r--r-- 1 sebulba Administ 4478 Apr 29 15:02 README.rst drwxr-xr-x 2 sebulba Administ 4096 Apr 27 12:18 plumbum -rw-r--r-- 1 sebulba Administ 0 Apr 27 11:54 setup.py drwxr-xr-x 2 sebulba Administ 0 Apr 27 11:54 tests -rw-r--r-- 1 sebulba Administ 18 Apr 27 11:54 todo.txt .. note:: The output of ``ls`` went straight to the screen :: >>> ls["-a"] & BG >>> f = _ >>> f.ready() False >>> f.wait() >>> f.stdout '.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\nplumbum\n[...]' If you want to redirect the output, you can pass those arguments to the BG modifier. So the command ``ls & BG(stdout=sys.stdout, stderr=sys.stderr)`` has exactly the same effect as ``ls &`` in a terminal. You can also start a long running process and detach it in ``nohup`` mode using the ``NOHUP`` modifier:: >>> ls["-a"] & NOHUP If you want to redirect the input or output to something other than ``nohup.out``, you can add parameters to the modifier:: >>> ls["-a"] & NOHUP(stdout='/dev/null') # Or None .. versionadded:: 1.6 The ``NOHUP`` modifier You can also use the ``TEE`` modifier, which causes output to be redirected to the screen (like ``FG``), but also provides access to the output (like ``BG``). .. _guide-local-commands-nesting: Command Nesting --------------- The arguments of commands can be strings (or any object that can meaningfully-convert to a string), as we've seen above, but they can also be other **commands**! This allows nesting commands into one another, forming complex command objects. The classic example is ``sudo``:: >>> from plumbum.cmd import sudo >>> print(sudo[ls["-l", "-a"]]) /usr/bin/sudo /bin/ls -l -a >>> sudo[ls["-l", "-a"]]() 'total 22\ndrwxr-xr-x 8 sebulba Administ 4096 May 9 20:46 .\n[...]' In fact, you can nest even command-chains (i.e., pipes and redirections), e.g., ``sudo[ls | grep["\\.py"]]``; however, that would require that the top-level program be able to handle these shell operators, and this is not the case for ``sudo``. ``sudo`` expects its argument to be an executable program, and it would complain about ``|`` not being one. So, there's a inherent difference between between ``sudo[ls | grep["\\.py"]]`` and ``sudo[ls] | grep["\\.py"]`` (where the pipe is unnested) -- the first would fail, the latter would work as expected. Some programs (mostly shells) will be able to handle pipes and redirections -- an example of such a program is ``ssh``. For instance, you could run ``ssh["somehost", ls | grep["\\.py"]]()``; here, both ``ls`` and ``grep`` would run on ``somehost``, and only the filtered output would be sent (over SSH) to our machine. On the other hand, an invocation such as ``(ssh["somehost", ls] | grep["\\.py"])()`` would run ``ls`` on ``somehost``, send its entire output to our machine, and ``grep`` would filter it locally. We'll learn more about remote command execution :ref:`later `. In the meanwhile, we should learn that command nesting works by *shell-quoting* (or *shell-escaping*) the nested command. Quoting normally takes place from the second level of nesting:: >>> print(ssh["somehost", ssh["anotherhost", ls | grep["\\.py"]]]) /bin/ssh somehost /bin/ssh anotherhost /bin/ls '|' /bin/grep "'\\.py'" In this example, we first ssh to ``somehost``, from it we ssh to ``anotherhost``, and on that host we run the command chain. As you can see, ``|`` and the backslashes have been quoted, to prevent them from executing on the first-level shell; this way, they would safey get to the second-level shell. For further information, see the :ref:`api docs `. plumbum-1.8.3/docs/local_machine.rst0000644000000000000000000001010114613634536014376 0ustar00.. _guide-local-machine: The Local Object ================ So far we've only seen running local commands, but there's more to the ``local`` object than this; it aims to "fully represent" the *local machine*. First, you should get acquainted with ``which``, which performs program name resolution in the system ``PATH`` and returns the first match (or raises an exception if no match is found):: >>> local.which("ls") >>> local.which("nonexistent") Traceback (most recent call last): [...] plumbum.commands.CommandNotFound: ('nonexistent', [...]) Another member is ``python``, which is a command object that points to the current interpreter (``sys.executable``):: >>> local.python >>> local.python("-c", "import sys;print(sys.version)") '3.10.0 (default, Feb 2 2022, 02:22:22) [MSC v.1931 64 bit (Intel)]\r\n' Working Directory ----------------- The ``local.cwd`` attribute represents the current working directory. You can change it like so:: >>> local.cwd >>> local.cwd.chdir("d:\\workspace\\plumbum\\docs") >>> local.cwd You can also use it as a *context manager*, so it behaves like ``pushd``/``popd``:: >>> ls_l = ls | wc["-l"] >>> with local.cwd("c:\\windows"): ... print(f"{local.cwd}:{ls_l()}") ... with local.cwd("c:\\windows\\system32"): ... print(f"{local.cwd}:{ls_l()}") ... c:\windows: 105 c:\windows\system32: 3013 >>> print(f"{local.cwd}:{ls_l()}") d:\workspace\plumbum: 9 Finally, A more explicit and thread-safe way of running a command in a different directory is using the ``.with_cwd()`` method: >>> ls_in_docs = local.cmd.ls.with_cwd("docs") >>> ls_in_docs() 'api\nchangelog.rst\n_cheatsheet.rst\ncli.rst\ncolorlib.rst\n_color_list.html\ncolors.rst\nconf.py\nindex.rst\nlocal_commands.rst\nlocal_machine.rst\nmake.bat\nMakefile\n_news.rst\npaths.rst\nquickref.rst\nremote.rst\n_static\n_templates\ntyped_env.rst\nutils.rst\n' Environment ----------- Much like ``cwd``, ``local.env`` represents the *local environment*. It is a dictionary-like object that holds **environment variables**, which you can get/set intuitively:: >>> local.env["JAVA_HOME"] 'C:\\Program Files\\Java\\jdk1.6.0_20' >>> local.env["JAVA_HOME"] = "foo" And similarity to ``cwd`` is the context-manager nature of ``env``; each level would have it's own private copy of the environment:: >>> with local.env(FOO="BAR"): ... local.python("-c", "import os; print(os.environ['FOO'])") ... with local.env(FOO="SPAM"): ... local.python("-c", "import os; print(os.environ['FOO'])") ... local.python("-c", "import os; print(os.environ['FOO'])") ... 'BAR\r\n' 'SPAM\r\n' 'BAR\r\n' >>> local.python("-c", "import os;print(os.environ['FOO'])") Traceback (most recent call last): [...] ProcessExecutionError: Unexpected exit code: 1 Command line: | /usr/bin/python3 -c "import os; print(os.environ['FOO'])" Stderr: | Traceback (most recent call last): | File "", line 1, in | File "/usr/lib/python3.10/os.py", line 725, in __getitem__ | raise KeyError(key) from None | KeyError: 'FOO' In order to make cross-platform-ness easier, the ``local.env`` object provides some convenience properties for getting the username (``.user``), the home path (``.home``), and the executable path (``path``) as a list. For instance:: >>> local.env.user 'sebulba' >>> local.env.home >>> local.env.path [, , ...] >>> >>> local.which("python") >>> local.env.path.insert(0, "c:\\python310") >>> local.which("python") For further information, see the :ref:`api docs `. plumbum-1.8.3/docs/make.bat0000644000000000000000000001201214613634536012476 0ustar00@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PlumbumShellCombinators.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PlumbumShellCombinators.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end plumbum-1.8.3/docs/paths.rst0000644000000000000000000001016314613634536012747 0ustar00 .. _guide-paths: Paths ===== Apart from commands, Plumbum provides an easy to use path class that represents file system paths. Paths are returned from several plumbum commands, and local paths can be directly created by :func:`local.path() `. Paths are always absolute and are immutable, may refer to a remote machine, and can be used like a ``str``. In many respects, paths provide a similar API to pathlib in the Python 3.4+ standard library, with a few improvements and extra features. .. versionadded:: 1.6 Paths now support more pathlib like syntax, several old names have been depreciated, like ``.basename`` The primary ways to create paths are from ``.cwd``, ``.env.home``, or ``.path(...)`` on a local or remote machine, with ``/``, ``//`` or ``[]`` for composition. .. note:: The path returned from ``.cwd`` can also be used in a context manager and has a ``.chdir(path)`` function. See :ref:`guide-local-machine` for an example. Paths provide a variety of functions that allow you to check the status of a file:: >>> p = local.path("c:\\windows") >>> p.exists() True >>> p.is_dir() True >>> p.is_file() False Besides checking to see if a file exists, you can check the type of file using ``.is_dir()``, ``is_file()``, or ``is_symlink()``. You can access details about the file using the properties ``.dirname``, ``.drive``, ``.root``, ``.name``, ``.suffix``, and ``.stem`` (all suffixes). General stats can be obtained with ``.stat()``. You can use ``.with_suffix(suffix, depth=1)`` to replace the last ``depth`` suffixes with a new suffix. If you specify None for the depth, it will replace all suffixes (for example, ``.tar.gz`` is two suffixes). Note that a name like ``file.name.10.15.tar.gz`` will have "5" suffixes. Also available is ``.with_name(name)``, which will will replace the entire name. ``preferred_suffix(suffix)`` will add a suffix if one does not exist (for default suffix situations). Paths can be composed using ``/`` or ``[]``:: >>> p / "notepad.exe" >>> (p / "notepad.exe").is_file() True >>> (p / "notepad.exe").with_suffix(".dll") >>> p["notepad.exe"].is_file() True >>> p["../some/path"]["notepad.exe"].with_suffix(".dll") You can also iterate over directories to get the contents:: >>> for p2 in p: ... print(p2) ... c:\windows\addins c:\windows\appcompat c:\windows\apppatch ... Paths also supply ``.iterdir()``, which may be faster on Python 3.5. Globing can be easily performed using ``//`` (floor division):: >>> p // "*.dll" [, ...] >>> p // "*/*.dll" [, ...] >>> local.cwd / "docs" // "*.rst" [, ...] .. versionadded:: 1.6 Globing a tuple will glob for each of the items in the tuple, and return the aggregated result. Files can be opened and read directly:: >>> with(open(local.cwd / "docs" / "index.rst")) as f: ... print(read(f)) <...output...> .. versionadded:: 1.6 Support for treating a path exactly like a ``str``, so they can be used directly in ``open()``. Paths also supply ``.delete()``, ``.copy(destination, override=False)``, and ``.move(destination)``. On systems that support it, you can also use ``.symlink(destination)``, ``.link(destination)``, and ``.unlink()``. You can change permissions with ``.chmod(mode)``, and change owners with ``.chown(owner=None, group=None, recursive=None)``. If ``recursive`` is ``None``, this will be recursive only if the path is a directory. For **copy**, **move**, or **delete** in a more general helper function form, see the :ref:`utils modules `. Relative paths can be computed using ``.relative_to(source)`` or ``mypath - basepath``, though it should be noted that relative paths are not as powerful as absolute paths, and are primarily for recording a path or printing. For further information, see the :ref:`api docs `. plumbum-1.8.3/docs/quickref.rst0000644000000000000000000002323614613634536013446 0ustar00.. _guide-quickref: Quick reference guide --------------------- This is a cheatsheet for common tasks in Plumbum. CLI === Optional arguments ****************** ================ ========================= Utility Usage ================ ========================= ``Flag`` True or False descriptor ``SwitchAttr`` A value as a descriptor ``CountOf`` Counting version of ``Flag`` ``@switch`` A function that runs when passed ``@autoswitch`` A switch that gets its name from the function decorated ``@validator`` A positional argument validator on main (or use Py3 attributes) ================ ========================= Validators ********** Anything that produces a ``ValueError`` or ``TypeError``, is applied to argument. Some special ones included: ======================= ========================= Validator Usage ======================= ========================= ``Range`` A number in some range ``Set`` A choice in a set ``ExistingFile`` A file (converts to Path) ``ExistingDirectory`` A directory ``NonexistentPath`` Not a file or directory ======================= ========================= Common options ************** ================== ============================ ================== Option Used in Usage ================== ============================ ================== First argument Non-auto The name, or list of names, includes dash(es) Second argument All The validator docstring ``switch``, ``Application`` The help message ``help=`` All The help message ``list=True`` ``switch`` Allow multiple times (passed as list) ``requires=`` All A list of optional arguments to require ``excludes=`` All A list of optional arguments to exclude ``group=`` All The name of a group ``default=`` All The default if not given ``envname=`` ``SwitchAttr`` The name of an environment variable to check ``mandatory=True`` Switches Require this argument to be passed ================== ============================ ================== Special member variables ************************ ====================== ===================================== Utility Usage ====================== ===================================== ``PROGNAME=`` Custom program name and/or color ``VERSION=`` Custom version ``DESCRIPTION=`` Custom description (or use docstring) ``DESCRIPTION_MORE=`` Custom description with whitespace ``ALLOW_ABREV=True`` Allow argparse style abbreviations ``COLOR_USAGE=`` Custom color for usage statement ``COLOR_USAGE_TITLE=`` Custom color for usage statement's title ``COLOR_GROUPS=`` Colors of groups (dictionary) ``COLOR_GROUP_TITLES=`` Colors of group titles (dictionary) ====================== ===================================== Paths ===== ================= ============================= Idiom Description ================= ============================= ``local.cwd`` Common way to make paths ``/`` Construct Composition of parts ``//`` Construct Grep for files Sorting Alphabetical Iteration By parts To str Canonical full path Subtraction Relative path ``in`` Check for file in folder ================= ============================= .. The main difference is the loss of relative files =================================================== =========================== ================== Property Description Compare to Pathlib =================================================== =========================== ================== ``.name`` The file name ✓ ``.basename`` DEPRECATED ``.stem`` Name without extension ✓ ``.dirname`` Directory name ✗ ``.root`` The file tree root ✓ ``.drive`` Drive letter (Windows) ✓ ``.suffix`` The suffix ✓ ``.suffixes`` A list of suffixes ✓ ``.uid`` User ID ✗ ``.gid`` Group ID ✗ ``.parts`` Tuple of ``split`` ✓ ``.parents`` The ancestors of the path ✓ ``.parent`` The ancestor of the path ✓ =================================================== =========================== ================== .. Missing: .anchor =================================================== =========================== ================== Method Description Compare to Pathlib =================================================== =========================== ================== ``.up(count = 1)`` Go up count directories ✗ ``.walk(filter=*, dir_filter=*)`` Traverse directories ✗ ``.as_uri(scheme=None)`` Universal Resource ID ✓ ``.join(part, ...)`` Put together paths (``/``) ``.joinpath`` ``.list()`` Files in directory ✗ (shortcut) ``.iterdir()`` Fast iterator over dir ✓ ``.is_dir()`` If path is dir ✓ ``.isdir()`` DEPRECATED ``.is_file()`` If is file ✓ ``.isfile()`` DEPRECATED ``.is_symlink()`` If is symlink ✓ ``.islink()`` DEPRECATED ``.exists()`` If file exists ✓ ``.stat()`` Return OS stats ✓ ``.with_name(name)`` Replace filename ✓ ``.with_suffix(suffix, depth=1)`` Replace suffix ✓ (no depth) ``.preferred_suffix(suffix)`` Replace suffix if no suffix ✗ ``.glob(pattern)`` Search for pattern ✓ ``.split()`` Split into directories ``.parts`` ``.relative_to(source)`` Relative path (``-``) ✓ ``.resolve(strict=False)`` Does nothing ✓ ``.access(mode = 0)`` Check access permissions ✗ =================================================== =========================== ================== .. Missing: .match(pattern) .is_reserved() .is_absolute() .as_posix() .is_symlink() .is_fifo() .is_block_device() .is_char_device() .lchmod(mode) .lstat() =================================================== =========================== ================== Method (changes files) Description Compare to Pathlib =================================================== =========================== ================== ``.link(dst)`` Make a hard link ✗ ``.symlink(dst)`` Make a symlink ``.symlink_to`` ``.unlink()`` Unlink a file (delete) ✓ ``.delete()`` Delete file ``.unlink`` ``.move(dst)`` Move file ✗ ``.rename(newname)`` Change the file name ✓ ``.copy(dst, override=False)`` Copy a file ✗ ``.mkdir()`` Make a directory ✓ (+ more args) ``.open(mode="r")`` Open a file for reading ✓ (+ more args) ``.read(encoding=None)`` Read a file to text ``.read_text`` ``.write(data, encoding=None)`` Write to a file ``.write_text`` ``.touch()`` Touch a file ✓ (+ more args) ``.chown(owner=None, group=None, recursive=None)`` Change owner ✗ ``.chmod(mode)`` Change permissions ✓ =================================================== =========================== ================== .. Missing: .group() .owner() .read_bytes() .write_bytes() .replace(target) .rglob(pattern) .rmdir() .samefile() Colors ====== You pick colors from ``fg`` or ``bg``, also can ``reset`` Main colors: ``black`` ``red`` ``green`` ``yellow`` ``blue`` ``magenta`` ``cyan`` ``white`` Default styles: ``warn`` ``title`` ``fatal`` ``highlight`` ``info`` ``success`` Attrs: ``bold`` ``dim`` ``underline`` ``italics`` ``reverse`` ``strikeout`` ``hidden`` plumbum-1.8.3/docs/remote.rst0000644000000000000000000002375014613634536013131 0ustar00.. _guide-remote: Remote ====== Just like running local commands, Plumbum supports running commands on remote systems, by executing them over SSH. .. _guide-remote-machines: Remote Machines --------------- Forming a connection to a remote machine is very straight forward:: >>> from plumbum import SshMachine >>> rem = SshMachine("hostname", user = "john", keyfile = "/path/to/idrsa") >>> # ... >>> rem.close() Or as a context-manager:: >>> with SshMachine("hostname", user = "john", keyfile = "/path/to/idrsa") as rem: ... pass .. note:: ``SshMachine`` requires ``ssh`` (``openSSH`` or compatible) installed on your system in order to connect to remote machines. The remote machine must have bash as the default shell (or any shell that supports the ``2>&1`` syntax for stderr redirection). Alternatively, you can use the pure-Python implementation of :ref:`ParamikoMachine `. Only the ``hostname`` parameter is required, all other parameters are optional. If the host has your ``id-rsa.pub`` key in its ``authorized_keys`` file, or if you've set up your ``~/.ssh/config`` to login with some user and ``keyfile``, you can simply use ``rem = SshMachine("hostname")``. Much like the :ref:`local object `, remote machines expose ``which()``, ``path()``, ``python``, ``cwd`` and ``env``. You can also run remote commands, create SSH tunnels, upload/download files, etc. You may also refer to :class:`the full API `, as this guide will only survey the features. .. note:: `PuTTY `_ users on Windows should use the dedicated :class:`PuttyMachine ` instead of ``SshMachine``. See also :ref:`ParamikoMachine `. .. versionadded:: 1.0.1 Working Directory and Environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``cwd`` and ``env`` attributes represent the remote machine's working directory and environment variables, respectively, and can be used to inspect or manipulate them. Much like their local counterparts, they can be used as context managers, so their effects can be contained. :: >>> rem.cwd >>> with rem.cwd(rem.cwd / "Desktop"): ... print(rem.cwd) /home/john/Desktop >>> rem.env["PATH"] /bin:/sbin:/usr/bin:/usr/local/bin >>> rem.which("ls") Tunneling ^^^^^^^^^ SSH tunneling is a very useful feature of the SSH protocol. It allows you to connect from your machine to a remote server process, while having your connection authenticated and encrypted out-of-the-box. Say you run on ``machine-A``, and you wish to connect to a server program running on ``machine-B``. That server program binds to ``localhost:8888`` (where ``localhost`` refers naturally to to ``machine-B``). Using Plumbum, you can easily set up a tunnel from port 6666 on ``machine-A`` to port 8888 on ``machine-B``:: >>> tun = rem.tunnel(6666, 8888) >>> # ... >>> tun.close() Or as a context manager:: >>> with rem.tunnel(6666, 8888): ... pass You can now connect a socket to ``machine-A:6666``, and it will be securely forwarded over SSH to ``machine-B:8888``. When the tunnel object is closed, all active connections will be dropped. .. _guide-remote-commands: Remote Commands --------------- Like local commands, remote commands are created using indexing (``[]``) on a remote machine object. You can either pass the command's name, in which case it will be resolved by through ``which``, or the path to the program. :: >>> rem["ls"] , '/bin/ls')> >>> rem["/usr/local/bin/python3.2"] , '/usr/local/bin/python3.2')> >>> r_ls = rem["ls"] >>> r_grep = rem["grep"] >>> r_ls() 'foo\nbar\spam\n' Nesting Commands ^^^^^^^^^^^^^^^^ Remote commands can be nested just like local ones. In fact, that's how the ``SshMachine`` operates behind the scenes - it nests each command inside ``ssh``. Here are some examples:: >>> r_sudo = rem["sudo"] >>> r_ifconfig = rem["ifconfig"] >>> print(r_sudo[r_ifconfig["-a"]]()) eth0 Link encap:Ethernet HWaddr ... [...] You can nest multiple commands, one within another. For instance, you can connect to some machine over SSH and use that machine's SSH client to connect to yet another machine. Here's a sketch:: >>> from plumbum.cmd import ssh >>> print(ssh["localhost", ssh["localhost", "ls"]]) /usr/bin/ssh localhost /usr/bin/ssh localhost ls >>> >>> ssh["localhost", ssh["localhost", "ls"]]() 'bin\nDesktop\nDocuments\n...' Piping ^^^^^^ Piping works for remote commands as well, but there's a caveat to note here: the plumbing takes place on the local machine! Consider this code for instance :: >>> r_grep = rem["grep"] >>> r_ls = rem["ls"] >>> (r_ls | r_grep["b"])() 'bin\nPublic\n' Although ``r_ls`` and ``r_grep`` are remote commands, the data is sent from ``r_ls`` to the local machine, which then sends it to the remote one for running ``grep``. This will be fixed in a future version of Plumbum. It should be noted, however, that piping remote commands into local ones is perfectly fine. For example, the previous code can be written as :: >>> from plumbum.cmd import grep >>> (r_ls | grep["b"])() 'bin\nPublic\n' Which is even more efficient (no need to send data back and forth over SSH). .. _guide-paramiko-machine: Redirection ^^^^^^^^^^^ Redirection to and from remote paths is not currently supported, but you can redirect to and from local paths, with the familiar syntax explained in :ref:`the corresponding section for local commands `. Note that if the redirection target/source is given as a string, it is automatically interpreted as a path on the local machine. Paramiko Machine ---------------- .. versionadded:: 1.1 ``SshMachine`` relies on the system's ``ssh`` client to run commands; this means that for each remote command you run, a local process is spawned and an SSH connection is established. While relying on a well-known and trusted SSH client is the most stable option, the incurred overhead of creating a separate SSH connection for each command may be too high. In order to overcome this, Plumbum provides integration for `paramiko `_, an open-source, pure-Python implementation of the SSH2 protocol. This is the ``ParamikoMachine``, and it works along the lines of the ``SshMachine``:: >>> from plumbum.machines.paramiko_machine import ParamikoMachine >>> rem = ParamikoMachine("192.168.1.143") >>> rem["ls"] RemoteCommand(, ) >>> r_ls = rem["ls"] >>> r_ls() 'bin\nDesktop\nDocuments\nDownloads\nexamples.desktop\nMusic\nPictures\n...' >>> r_ls("-a") '.\n..\n.adobe\n.bash_history\n.bash_logout\n.bashrc\nbin...' .. note:: Using ``ParamikoMachine`` requires paramiko to be installed on your system. Also, you have to explicitly import it (``from plumbum.machines.paramiko_machine import ParamikoMachine``) as paramiko is quite heavy. Refer to :class:`the API docs ` for more details. The main advantage of using ``ParamikoMachine`` is that only a single, persistent SSH connection is created, over which commands execute. Moreover, paramiko has a built-in SFTP client, which is used instead of ``scp`` to copy files (employed by the ``.download()``/``.upload()`` methods), and tunneling is much more light weight: In the ``SshMachine``, a tunnel is created by an external process that lives for as long as the tunnel is to remain active. The ``ParamikoMachine``, however, can simply create an extra *channel* on top of the same underlying connection with ease; this is exposed by ``connect_sock()``, which creates a tunneled TCP connection and returns a socket-like object .. warning:: Piping and input/output redirection don't really work with ``ParamikoMachine`` commands. You'll get all kinds of errors, like ``'ChannelFile' object has no attribute 'fileno'`` or ``I/O operation on closed file`` -- this is due to the fact that Paramiko's channels are not real, OS-level files, so they can't interact with ``subprocess.Popen``. This will be solved in a future release; in the meanwhile, you can use the machine's ``.session()`` method, like so :: >>> s = mach.session() >>> s.run("ls | grep b") (0, 'bin\nPublic\n', '') Tunneling Example ^^^^^^^^^^^^^^^^^ On ``192.168.1.143``, I ran the following sophisticated server (notice it's bound to ``localhost``):: >>> import socket >>> s=socket.socket() >>> s.bind(("localhost", 12345)) >>> s.listen(1) >>> s2,_=s.accept() >>> while True: ... data = s2.recv(1000) ... if not data: ... break ... s2.send("I eat " + data) ... On my other machine, I connect (over SSH) to this host and then create a tunneled connection to port 12345, getting back a socket-like object:: >>> rem = ParamikoMachine("192.168.1.143") >>> s = rem.connect_sock(12345) >>> s.send("carrot") 6 >>> s.recv(1000) 'I eat carrot' >>> s.send("babies") 6 >>> s.recv(1000) 'I eat babies' >>> s.close() .. _guide-remote-paths: Remote Paths ------------ Analogous to local paths, remote paths represent a file-system path of a remote system, and expose a set of utility functions for iterating over subpaths, creating subpaths, moving/copying/ renaming paths, etc. :: >>> p = rem.path("/bin") >>> p / "ls" >>> (p / "ls").is_file() True >>> rem.path("/dev") // "sd*" [, < RemotePath /dev/sdb>, , ] .. note:: See the :ref:`guide-utils` guide for copying, moving and deleting remote paths For further information, see the :ref:`api docs `. plumbum-1.8.3/docs/typed_env.rst0000644000000000000000000000561414613634536013632 0ustar00.. _guide-typed-env: TypedEnv ======== Plumbum provides this utility class to facilitate working with environment variables. Similar to how :class:`plumbum.cli.Application` parses command line arguments into pythonic data types, :class:`plumbum.typed_env.TypedEnv` parses environment variables: class MyEnv(TypedEnv): username = TypedEnv.Str("USER", default='anonymous') path = TypedEnv.CSV("PATH", separator=":", type=local.path) tmp = TypedEnv.Str(["TMP", "TEMP"]) # support 'fallback' var-names is_travis = TypedEnv.Bool("TRAVIS", default=False) # True is 'yes/true/1' (case-insensitive) We can now instantiate this class to access its attributes:: >>> env = MyEnv() >>> env.username 'ofer' >>> env.path [, , , , , , ] >>> env.tmp Traceback (most recent call last): [...] KeyError: 'TMP' >>> env.is_travis False Finally, our ``TypedEnv`` object allows us ad-hoc access to the rest of the environment variables, using dot-notation:: >>> env.HOME '/home/ofer' We can also update the environment via our ``TypedEnv`` object: >>> env.tmp = "/tmp" >>> env.tmp '/tmp' >>> from os import environ >>> env.TMP '/tmp' >>> env.is_travis = True >>> env.TRAVIS 'yes' >>> env.path = [local.path("/a"), local.path("/b")] >>> env.PATH '/a:/b' TypedEnv as an Abstraction Layer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``TypedEnv`` class is very useful for separating your application from the actual environment variables. It provides a layer where parsing and normalizing can take place in a centralized fashion. For example, you might start with this simple implementation:: class CiBuildEnv(TypedEnv): job_id = TypedEnv.Str("BUILD_ID") Later, as the application gets more complicated, you may expand your implementation like so:: class CiBuildEnv(TypedEnv): is_travis = TypedEnv.Bool("TRAVIS", default=False) _travis_job_id = TypedEnv.Str("TRAVIS_JOB_ID") _jenkins_job_id = TypedEnv.Str("BUILD_ID") @property def job_id(self): return self._travis_job_id if self.is_travis else self._jenkins_job_id TypedEnv vs. local.env ^^^^^^^^^^^^^^^^^^^^^^ It is important to note that ``TypedEnv`` is separate and unrelated to the ``LocalEnv`` object that is provided via ``local.env``. While ``TypedEnv`` reads and writes directly to ``os.environ``, ``local.env`` is a frozen copy taken at the start of the python session. While ``TypedEnv`` is focused on parsing environment variables to be used by the current process, ``local.env``'s primary purpose is to manipulate the environment for child processes that are spawned via plumbum's :ref:`local commands `. plumbum-1.8.3/docs/utils.rst0000644000000000000000000000330014613634536012763 0ustar00.. _guide-utils: Utilities ========= The ``utils`` module contains a collection of useful utility functions. Note that they are not imported into the namespace of ``plumbum`` directly, and you have to explicitly import them, e.g. ``from plumbum.path.utils import copy``. * :func:`copy(src, dst) ` - Copies ``src`` to ``dst`` (recursively, if ``src`` is a directory). The arguments can be either local or remote paths -- the function will sort out all the necessary details. * If both paths are local, the files are copied locally * If one path is local and the other is remote, the function uploads/downloads the files * If both paths refer to the same remote machine, the function copies the files locally on the remote machine * If both paths refer to different remote machines, the function downloads the files to a temporary location and then uploads them to the destination * :func:`move(src, dst) ` - Moves ``src`` onto ``dst``. The arguments can be either local or remote -- the function will sort our all the necessary details (as in ``copy``) * :func:`delete(*paths) ` - Deletes the given sequence of paths; each path may be a string, a local/remote path object, or an iterable of paths. If any of the paths does not exist, the function silently ignores the error and continues. For example :: from plumbum.path.utils import delete delete(local.cwd // "*/*.pyc", local.cwd // "*/__pycache__") * :func:`gui_open(path) ` - Opens a file in the default editor on Windows, Mac, or Linux. Uses ``os.startfile`` if available (Windows), ``xdg_open`` (GNU), or ``open`` (Mac). plumbum-1.8.3/docs/_static/fish-text-black.png0000644000000000000000000000440714613634536016223 0ustar00PNG  IHDRa9 sRGBgAMA aPLTE9999ffijkkk prty|pt ~ y (~(9ffff9ff99f& - 3 9006688@FIMOSZ`filqsr@@AAHHPPXX``hhppxxy| 9ff99fff!#%&()+-.0ۏ9ۏff/13ş۶ʧϯԷٿۏۏsґtRNSS% pHYsodtEXtSoftwarePaint.NET v3.5.87;]_IDATXGVWA'-E(YJ<2 S1 B]e]<Ιn\7⫳TVO3$jgU.*2n "!3'Y]:HĦ#43yb㉄ƍưZƖvs1@< J3n'i7 aqY8+!EJVۃ*g+Q2Bg|>M)S\G ڀy\$TP$FI/렞/g}04aD)c[sǿFnv)7AfOycDz 1Pqsͽl0֦水!h_,| ?(cϸ# w-F 础|r?3(Ê=)i< Hܙ`OJ]u! /b C*L,Ni/(hK9RY-tZutAx6e- I' !MؙSusX @1j[D(J$xҳ+2F.Rܦ7D@aRǎԐJofJv["`聆%IbN0#KFI>4oR- Mj9cLy!bU{ *X[;ƚd]aH[.C1erX.Ȇ}&*׃S Iq5\VL,*f@(m9)M7t8% #PGk̝X}E-gW:*uj qh _K#y/#\nz~rjk2[~)Fu<͘bHM/z=p߃?≚+Ƥ@xSDDDAGF@q Dg,}ƒaHQ~i'M?B9O c/ -|0}yث^Cbln~q)\h)x{ſ[u ,hcñIENDB`plumbum-1.8.3/docs/_static/github-logo.png0000644000000000000000000000540414613634536015454 0ustar00PNG  IHDRd-XsRGBgAMA a pHYsttfxtEXtSoftwarePaint.NET v3.5.87;] tIDATx^ UUAE|0>p uT5|t)4GQ ELy10H mM 2G"OڬZs̹3{_8}4[[0Upпͨw#lDݭie`s"n)ߴnV٤q 1u!5jP V@뒽EH., k} kQ@;B=֯by[& Ji aHPaʛr}ʼV J; )R*DDZoRDK l9)n#yC'"Ƙg}_$\/,N4 )36m }g/߁7%e%iSQoV2!1Y!BRm(xu(U^&>!5)!Jt& MؘZ!OGvm$&%z_*̉(yΔ ZHެoi5 sɲR%i& ODo" "!}Tc3CӦFZ3d]]4{&ӧ7<_#)˔ [^V!책kgr}J; -I=eîY囋i:^WcM# Q] l,,h[J>JxG6IH`O(dHL!FvMTS6 dr \%B0"::g1Cx9"AwfKՎVOMV'O("gBe0L@Yk   ȟ 3ڮFef RP[@Ӥx6HjqYQ dGB/+C7){&rR?Ga[ RvDV󩍾rlRBBwy8K8S_ Lm;_jb؛ _ kB؁XTR ĬS88BHp-)9AtySZŹKKCA7(ŏqy-b:{o$*f "Bۅ#<Ņ>`ȑ̯|W*UIW$ [ / LS='cr3}Uo ?.g'|DmJ'U@I6Jydrgmw\TRԷ "mBӷ1!]ϬpDɅ5SVȅbQ=om zǏqy\6(Fwf.'ޞY@ 79tڵ`|ί:s0@ ///#s|oIBB+/״R5ᔰo`o{V9!W->0W _$03'"2K0[3Äq{]\{+TOɛA'#4e03 &LMN \)%qJm[~guFo]zcen| Qp-k*e?vg6SSM8B]eG)ݝE_[J{VDV4rTw BXƢ2!L7 +=`.gYl*< RNM FB3ZX"hKcA)oZ_W q3GUw|',pP _[P!h( "Ȓq&ps,}bMdUL@FF2e8cQɪ<聜~+sϣ,=cI2#Q=C➁|0 1 _,vr2k&A/bJ\!D֩VkyzHD]mpqY8QP8MF*NXqg :Fh#P S]`+MC~H=C$ *>1O{+PA&7"ișY^>zcӃ![3ŀ ay *.IENDB`plumbum-1.8.3/docs/_static/logo.png0000644000000000000000000002157514613634536014203 0ustar00PNG  IHDRb:WsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.100r"IDATx^?ȯŕop%jvpYEH!+EYPYl%Ia&@b),,b`n=7c>33s=ý@b 1H $j0$"Tb 1H $~;v@b 1p0D"Yhb 1H $~1b99@b 1P$Bwk׮=?myj IǙH $0I$m@Es}8-N۞?,k8q8%Ȉ.0D"瑜[x#"ۚI$@L"8H"x=^nkH V[X1DN>ބ =D~ľe`X:G9WkL"-<&}NVdߩ#ef]k0D"qSD slP.-1 {RWIG$H$^^Ha}מ\Byў :,F,H$Xܾj([fK"^뢎ΌJv^pX]: 8>ZP$I$/,N"b 3&?y`-ԷX}sk#Ta {?=o]j9W!$پLA=Cd[=uy}WSfDs,6jFa+'[~מS[|{l| Oq;O{KZ@7 1w8PhV{b ᫾+]}=TϊM'mT90:3Lޥ(za~*Rh/ wHo?w%}< !>|# 1MbM[]t[W_kS&N2" dl7'&w{quyj2EHyPiUʓܗ"ʛ\7w:S{6" ~:pBw"{G5H B='6n$Rcd+N*v,dp 봬b5: 2"&S}ǜYn"amҤ[g\ʉsV0y 3Fx~NU%ՁV5T 碥Խa0#VDm>\x5$Frq.+`; ?C"UYN=iV_ 0;]K$s8N\&Ws{&ȄH|leU)a(ǵcI"#iD@dd lڅf"IfIذh~Dq5H'꟧M;&x/ʈIYa*E#EߑsܕC?$ p '#g 4wc\|Ԙb٩-3C>Ig2+o:HOlURC`M{nk)*At ~ 0 *Q_laOm{m{*&xu+%cw^0M6k|fEmMlǨ*\ڢp-|Kf)zWN}=T_B.%=9(?Hf\F$:e}OOlxBmޘbRG:t\@n\:c}P4Aw;D.HLS[d+\9<{J[þܐg25a#Z?M ҂r,DHh!O= jeu_QlVgjOJFa[+/AsDHC^]u)?y{"X1DU6,<@Ln*RC8lKxj~k l/z>1pq ]Ѿ#+Kb}SQ7{u$N9g?ڦH(M_ޡ *o{ [Bud} E^MY[$h$2!戄ؗ=+rɄڵw j~SٖSF 06t}kHx"֞KaZv΅LhU vNuVNd5Pp$÷{ zuޥyq޺ ~gC!9f(cù.?0{$/ƅEgM.tQK]ڴ&Y%ǫ܁'<}+ڳŢ.Ǥ1y4w%dwx%$C' LM$y+W'ZH^d?n}\H> m+%)ErѹçHvXn0"RB nu"bu1"!Gv9 4ٯ~5>Fz'Du.+a-] "ҪKV9Dipff? xHك4u ǃMGcG;јrh J>d,ZW/VfDG"<`%l-ጕuO[ywL 5px;C);0pxxB`h>X*o{!>+Tz"0/Ϯ }\עK[j( F[6]$լw $JQkǶiGVFXlS׉{BW?ɘ'L&]D*S}m?@$J%(`ׄ>5OQ6RU$p_y #nP`mkPO&-ǤF$IJIY+fzN 6Y']:lyhDK#=J"1kIOEwdl,sM੝xof&3h"AW~ I Y&UT`$MQ6!>O_kȖJ6ɥDI3lDbH ΌU-3Ɠlr>}gH}kCJ"q6f2t.7Ⱦe7$>1q&tْHTNSFäج _ޡ~Ӷ̑@Qۧ !F6#;&s65ז]JeVn* zDdM&-хM aD7U}QsDU#{6^޶ч I2ݫLJ H< x!OӶ"A 4P`cku1&wjT@Svm:fK`{V/T73X"O;mP0U'kWH4B%1 ۷YOHɐ:0my 0a"1ӷhhx]ZI$;iNHgOUgqDȨ[TN<..tJ1SuZTcBizǤ6̭#T\\ZrfνS_cNk.sd,\vD$)]Ip+H:#9&% }&jܭ:zls0"gV Ǣ2c5tMG6y$|q娯r͑np=>C"ϊl8dSu,q"8q N!a%sQaRrI!t2r:N`U%54ʚ:K24:v[`&ֱLHvp9!y }ͨ pt2n,Zm݃/Y v9!9DUG(3%RHn'ʒ[L CZ?kVPV$dvz( $liHydD$,=*D r;T'Swӭ玕>i,/H8A3:HQD+>m[M "AAY=Aɽy@`9 a)[aaGa=H## G"f3hhJ ϐވ\Gt@B\Oi"%Hd.&2О2Y}8TO@y"A=FtjcBran+尕58)Ed\E"A:(Aͧ _A٣c ߊF✌V"A²,9~g"R%0ldO4>MlbR_݁Y߈d #ut(aCmX0/m2TfΎ(\L@VM:-e{(DD"ɶk30D";HL ];"!+fk)gҩߩHp@Hc^M$3L4{j6SV5AP\D:1e-S2Yþo9& Pd~:aQ2I? hv9𥈄2}Bx:[Dی@!t@*t=mᓶRbRFy6Vd ,c$3ɤa*;/\ዸ-`D-(v}1"ѭڣr#Eݩ?޾?أ%]e{\酬N :d! &]}5_Rkf+R&U!.{#=Ea$^O$ 4_Ф/XNt!i "A/q^Y Ϛv!oʳ~H[L$E!kcJ#KD2C/y|&0DG"%'fD"n!I,$wHvjbѫURWqu}+$m$ nmda#e/ءONNw.k]D*4h?:"qc"AYCHtEʘ%$u BsADC0^MP IꜰM^"AxR @ rzBe'QܘH Wc\S@1DEAs#Va:ikMؿ(F2)&'Ͷ6LtAނwXu' _fmnшn!(=F$08R~Q5 A2y6~]u8vV7 =Ձ3ȑ,n 2^+v2? Il7&}#_1ʼnU&Rrjwdٱg m{>ru9tHv0.Z%troLv0@2"L5npE\Ӗ-9zM{WwHGUR#$0ĶD",!\=7'w8&6_",OO]F\H &wrŽ,+G/嬟^HGb֐DTy *a}Vjr&'hքVr-Xu`y܂5GBhJ\]jEЋ[cVdmO "хB^)'N>.ɽj;&~l.1yjE5_[=Ad%9"ۗH͞A:0ӄa[5 uG9 vɣָ`DhK [;1=iuPB @ yt@k?J.5+O⭑$~PWYEc:HɺBq\NYմ}I&Q\EbDCa7kL !lk"[Srgcr<2]{Hklu K{.Ff#}6YFm{gq#0/u>!z .lUD}2DB@Wp#:.In썢V;Q2qObaYmvʌ~u(j ?Or3}zG"9Hbز6¶pKlEeBVc8EtZJ㩱s,}}>25ݴvWOhtJƤʪ_{jy.Sc>{;=I?tzV?_M.ھO39Lx&9h[,au_盝~kjݷ_~JuhT ԉ?b-^Qa\Ϝy?yb"axv'Y}USY5 YZ[xV25 чvPBj|zxqM9&Utan!ӳ]KuyTb V}-DIe8FoS2鉘~^"A/AamS,5~MR BI+25]~Hi\R< %]oZӂn1o!F7k&Dod>Z`?/#?Vi5I$ NIGK.r{DX9C`"UaJA+G.$LnC=;Jq*B3&(:Cɛ}bro9٘%J>VxR#کiV'GQɑ@.vw},dԗ~ʉ(J$"ڐu|6VcB㾊]|.@eAy厁m(M : !x4,[C"NHZ81N $8+Lmt[*H|EHw)3qH "3^@Y]0:߲ʥ+"'JmN"\HI $€HhKO'%n흩()'$-J1M[vntxʺOĀHڤ=6MM7)t-DL")G ˥3I $DTU݈;~2"}U,XN I$,Qd F$V-!6o['e'~ $!R?5@]?ot;N"N 仉@bI$ DEkD"ZXK9@bzH"q1L;1L $HdD"1p0pT31p| $r&Hә&G@A$JAx@vM $@T>-LTzOCqqI $$'/@!!Gk;mb 1HHtWkD #{osKu6]wƽ?11I $@c A9Nl5@2ҙ&0D"DO(H smb$ _EEJ}9ϧm]rV7BsYD ߳1u ZDW8'w%x D?jͼi>Lu?YWv[4 (-9B<$~\D];V ,vΏ[ 59UX?{)h`ڣ~F-wqlX\m!'͋"=sWǁ3#d(.Bo1n,H#j =#*QA7-fɈOl]lf&cfghvz3 (F#1*JУ{r Ds<μ%YAQKPK*hjDk+Gf0 3:M6"}ఓꈨtB;Lϧ:l`Bt ~t{@[K/|].pE$fw2E aN'5 b.t:fnh^lb'\~zyDW}Q5:_= ˎhCK1Q^ҼL= #zȍ ,A"D4jZΖ=6T-BDӖ淸3zPKE*XUѠ?i>f&$Q! !TRFbq/"W&AT|h>Bw IfHtb%QT|rAk I""' .AT-N=9e<-ʆhWJ\\D|? ]fqS(l\hO`sbz3;;E YXDK?)O`M{y[@T -:DުzmG;Q ?)rܜ'3M O^O=b3F"QKP`" {P7s _nz׍P[0жio!D<@/z4B4 "ec{q-))֡egaDX3 oulEwۧd d|%ެ/CAΑW뀆҃8%|ƾ ^A5O t'@fhC'8T LU D1BAy7xzyQ0A̞ iTш̄.7Rg %G4sT{.#SSȟ_͠e_}qboīn pO`vfƕOu1V^ b׋sc&xMlɕt2Q!X*kDET QT QT QT Q^*^ae"ܸ΂#"ڴڏϗ*v|2ͦ}`QV(xq:H::W5VTM;>P e>\͐޾)(E Fl'|t&N!-QlLšt~ g|U>9 iPf{iй(!AIۤ#F 5(|^///${Oz}Ÿx Rq HPBoELi (и 6K)9؍"6E\dkY" 9q I0 i8gCl=IJb/:rBԽT)(L0wSK!-QS\PLf^ YZL-ZkSσ$(*"}*v?ȝR (a}ִ9 @&9U@i|:O٦ UxiA֓-%Qg"޾&& DV+e=0ބZЊZUќ G_>RMVT&ꍶ`( @Hm.8^N"wGd%aTTEf] 7L#%A"!4eGX̻eC#T CA!hQ'p2c\@'[nႾ$P"F3%lW (/'&tU!4lAA_HBRKOE0 ٵ]f8=H0 HbdͷCx\h v^:)3.؎zJ>SQH͗`@0-1-n uk|?@&) < p> wsa19 u]}2}# qAazr]긝rn{tv0*w T`4 (IH lósRS6Qm_12L &ZAlx.Edt=I](Wx2i$zp$((VUv$(Pf!~dj&3|$؎y...H Cґ9$S:FAS:p.0#GABh!HH1R)DG3:4Yka%(߯O:4][p1Z%at(v%m{ y#3I}HQ0c2oJ:G>ũC)n ѐI]9u3gx^=6|"4f 薃6ht2,; *D."1|8!XwKXAOH@IpQ[J}ɬۇN4;,1-)&Rk2w l^9{/6'[Y\k_fbŸ &~?s N}-h]bC޴}OC_gf,A>!| ҍ3gi4,_s5+GTlsyܲ\ts%(a2EPjR5e\ԮXvGU夎yS%(R3^e1as+J?Rq ⼖Pp~A` 8 d =A$W 7(\ZhN%?fM YK3,X8}>KCJux p)@_FӋ= ?#QX_tM]YSgK KY:>\R\Z-prE.`<0[s%uJ^ NA8 ѕA@hdQwڋȯCts3ņXV#H$I\g>kWH:,k`7k`]C8 3=j_ScUCwlemr̍QP ,/PFڏH:Fn9\ZhBuoyY Fj1 θ ^:PЙAN@)j¦;T[IENDB`plumbum-1.8.3/docs/_static/logo4.png0000644000000000000000000001006314613634536014255 0ustar00PNG  IHDR` [sRGBgAMA aPLTEHHHbbbttt}tRNSS% pHYsodtEXtSoftwarePaint.NET v3.5.100r IDATx^ۂ* u9,!]W{|$k몾iF!w|e>mu[BCLR1o˫nVXJW3%t|Y2;})[ fiF}7obVV3m{K}_3͘~~e*}J.UW5Z|Dgad6%'*D_In M| hqпYޥ1 egiAXˇ6hxrwEQܪM+5Ƹ[EBC^k`lM="?W~\ 8D,۽{x4t&tsWCdp) OBGg(>~la?~"3jQ-)pEoZ'{HyFj/JXԙ%Hh|K釀6> 8hn $P\?r^IOH}hS $h(Tw&3ĥCQB8zP'N`MyZ$ ,sEͫkeSOA *L/ёz4(Og/|r ZǪп~ǽ8IiY EWJ6QaJP$Z/=]HڀmУzCV⺂ A_?kV_KyYr:I2_GK'by5MbADz@rP yid7rgZGA?k3+Ac`(KI݀ImZ<͑C+38q/AJz0j,t f҂Lr 董$!U M-§mdpҦe[սtH!diyX'Iޔ+17}%8iRmqB;0 a`d^[,I?rt(/ǥv|2N@ԡ_CT e/МcʾϘkoPiq/aYT;E50ֱ%ZùIix@<0mij'A-[A}J&ѧIKmR5Mr bm_T ܙ_yG[x2}B"z)ря nQlOAq*8#PLN=/qMߍQWoTOJfv~4$Sw@63=*ѧp#֕ToG4 $Nаߌk(ˍ)$j00{v1=>*N0"#Ѵ7'#}lw"aa}u5Ov5A(SӜk^S⢯6@,IJ_o{*|f3 l3;J_=j}Їhly߇\:J A민$e{hIȍOgܵ؇7;;2WG 1I_?"sf.>4{_zqv邼.{\|fݲxwluAx%i=v]bdlq ߡE6pp:aa= bZ˹+  dA3iAK=,FEʎʥ GJ}jDʁ(tss6|AD=8 \ ~C`m4BLh%sZM#8/?l N 4DD THE˴ hXE.nNGH/5ojA "RrIqY;c/shV"<=㧗XG;3Kܤx{C45vCs(xƂ?]wt W/'AG:hl1*XÔ S"qny_Wŏ11~ȴ K?Rd¸^ʐY)U>@Ov/U .^U)bNjyܾZjO?s{r8{"u5Y $cJ_C߸J4&:ԭY,р٦?*:=<6q[vi 1SkwKgFG>J5AJ&cB쥬}Я JKF|-bDYcm%ҿ_~ނYs뿓^?aQ&ΏlK͹{ZmI<6¹ iW6ؽZ7}pXG:v֍~֑bQ~cWuQ+ý2]/,v{ggD#VhJ_ߥsh-a{_4ollۿ6koZg&H¦$ .O$/$JSn9_6[s,џcgv(̃EIENDB`plumbum-1.8.3/docs/_static/logo6.png0000644000000000000000000022154014613634536014263 0ustar00PNG  IHDR=6sRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.100rIDATx^xɖ% Oyzof޼=s[QU{'@{$$N8$9{EJ%9(2#####bޱc_(ePeP __@w}-qm? }]AiR"6l6l ?tKO-P" 4H_SHo}HOmHw-Fkc ] v讲.*奄Uqz"`d`cP0Ҍv ]i_:#ʎl6l6>!q@PWCMݥGOkݔ4 6=A]+1ؐޚ譊Go}tB}EeGSUEW=@0܀apKO}珥,r0QePe.v~6pOӓxTFShyƌSM?g7mjx|YQȻd{,h)C}~,*tbw:i证vȉ?`cxpxݧSRwuBNl6l6>Hm,qOoo[52vxpWcigSy:jsa4r! zB @8W}.Λ9eh͹jJ$뜳h:X')S\rUI_N,B0/t=;O>tTa}iOOQ珥,r0QePePԘ;F0U֊(N9z%}0ҚpŸ tͶ"fnxu}xmgy;|cpO!- |.B8[orx>V"֝炜ǑǗTRB)m@m@6޾7ِHt٣+9#9W);6{asH %/EGYRyI3tfЦ- CO1[ʌSIJ,7&;aAj;OPoC@[@_=珥,r0QePeu|P ȼ[8guh $#׭[!5 4% VHd~gsm2hl֔;论XU^H5a&  ZSX|_p'p:-.=<2{ƞ(a~oo_n"==]-2XeʎePexWm@nBcށοhC}iHa\ƚ#z'X~0Oqط:Kg/ukf$P|DGiF!,`S &;?O`Xǡ00MpƳQHCUIօ-{_s)ߗwQ83^e{˟7v? 6*{<2J>ʯ! .H(0NqY8c='#x5W!p-9A)]CY|0 FS_|g[2 ^%krmtGr[&k#Tga,p<{us \؏QN8'n~O}喺 }um`2z>Cy}m@nBߴԖ>ʈ?GuvJÐw7|5)q& /[j3&$K[C#{p| /b{Oq] UDoEtĢ-;\F5t>0].Y̹j0;Xn=Zc:c`FH8_TiE`=sK(ރmSS?!~ m,aGcyhӫȋ?H9*n媼EC5KݰEJ.BikCKj(#`);ɿOa==.m]Opۇ;ڸN#ǝzEQ2Rbl3 6ĭlm)Bj] j?8[v8m]F:"C҅i5KwE5[F ۍ8t²1W!n-'t2g Wthz;?߮~h_Bc?A]:6nrke "t} 4|{vݕqQZZ:jTTVޣPmJV鴳^ Ǘ~+9UpwPr ,T Ѻ0YJUqͯ !=.N83Z6)AKzޘ,P~Ǚ~-q}?BtI*b$.?ɆU]->53*Q\JwpJ{tڀmSIv6vޫq3_wV?-zҧgP5h'G" LѐjZS+ЙVkqNns- Ri ՙL@yJJ@#M !tzh|mZ0|H4bۇs]@ݓNa##{(Q63dyۦɇ:NMhb׵y. f=gEn\v݊[a!hL;|Jkr=(R>eک7՛SX $q3nףk3f 4Кr\Kt.\u~y\#sSZ 4>p=2' .=ҏ샗ݝnjO`caw0\,$uٓb"6*Jr*LHF$Rd`'xL ʊ (K^F('{ M)#5Y_w}"5TڝxoE,hS)nsM hu3?iy?+B=*͵K3QSb*||Ѷ[5{P'И -~x,N.C>d^AeʮX#&rՑ'P=ܩAhGq0}_q䜵>Tp_yn)g Y}UY='2x*2(ŋS-<ʛ"&#(E7훞꺛yO7.IL%qt>ٷ(FzD2]~yB+1qQ򦝬Gt~/&웓} EMhL_jK ?C⧑~ Q6;h46 jzU954]Ř$|I4>N? !Œ{?.z!)–MqR-QtՃꎸAf0#i['=L6}_Z:~ŏPT\_YQwݩ" ]};yE(o^PJMgN.,3du9Җ=G^򞎖`;yLn"yɛMTd uu9-:)VtL"qLY!﷖7"Ry*noH^U,L(Bj}=˫2U.cHRs)SF0\q@N Z#{AJ6Gv1<:eu܄{hfhx(vU]TBe T_Aeyd:AjFq܊h͸fn+ھqd AcF+IEC}Y J7u5yE''O:yA3E޴+MtYiY~]ې~S Ce)LkkK:m?]OŠHoC^b,ݛ$hjTm@n^>h%q$E1RQF?2淒Վk {o04`;AK3q[RhvǙD98oq=zࡽV@sç8785e`,5,T~#s֊,{*BBH!._}LZN6ɮ+RgIME((LASyWjY'"y%"@'wQ:ᑧ=[Wg(_rOVԟ@nBc¡T*_ xlza 85|[($i{M/hsĽ] 5PF~b"-YU8kmZX,ֆO97?`ŽŞEʏyB12]@kM!fVu !+3)5oymAbxQ>c:!E᷹&;1ILXg껽jh%WEYwQ4ץWD Pw@V;]:섷Iܟq-|𛟐?"ya9?'qOj8ui ׃wKZ&\nW1Z-.?c`c7X 7~}᪷Hz9Oq+N 򑷃 5Ltpy7gɛL2ܩD̉3gm\-`3Oϛz%ǧST=j6&3$d&R:9o q]ڃ GaĽG7R*mglJۆ[$6w.IλݡGJAZaD/-\t;55YS+kXEXu_a%~͔7ycqp%{T]p5:_;-=m60Qd?AJQ:S4?gVmgH[)T&_wl˔Eʣ3Uh}*ޔV*W7oRM*:FR܄6.aivI!쵶YH{ 6S&q[~=_~pSӛ[Hոw ǣ6 JD+5ZmQ[_c߲?-s H;| *?:Tݛ"Mr\ގS)d[t3)+K`w]W7Tڎ"c>S)ڐ܄6.aynq'cIvJ^/p8 fgxۨmE)fװ)l2w; ~@ZE 7 5u8K#Vpf+F:zYk??bۏ{z(q_ %`T6WF[COvby)b+O~ohrMTVQ:HEk3-@9Ҿ"K,9Ve?PM+3LM%rt:x#aw Ծa%ےkV>OxkiF>#_PvV ' V"ʘjt8+"9 x+qĽW+>ŽſǦ~M%Y [e qqBl@)wkCE܊XOGE/hSJo0DOufHxNU2}dNh)A&6R q祛'qhl p:n:`wh9P %`.II6T[Qv{6TsVPk4N #qGS q؄;pBBsӗ8qor!q]'^5 ,0#qGآ"nE:pY9`M<`;OX^Sm'n-W͙FSe?͏O3.I57őR]OI{#5Jl_ÈmfQe.ֺdsl[ku>{*#nE<)(drx.)b$u!O[QMJL}>E~h qg?M09&%:ZSX `c*$q=X4?J|9q/Eފl5sb-Xo-a$nspd'ؽ[36ʷeiHF{JiΆq"q+٧.wpT)UoVO>eɻʠ yVI?~0ˏ'n3cĽ6p7׷M=͔YT߯-b?#=u w;Ľ$ڋ(iDbi.cṡO}?If:_BȌ>n`L掺;$P{T;O+Tk:*WTu=Sɟc@8~SVKܦ\6;g:Əa,̆F,AMHܶ*_I[8s$:b#`$[́lA6WdDw\Wʏ8JUE m˽ߏ9`qea8y''"ݻ&әWEo&76$Swy3OܩIwM\*7RkaBOev$n#a(UQuŶ0+9OrzM+ϧ}-x!5J !] :?ކ?Qft0a-aַվd=<ԾqJݑƫ$⎱_e4\/3\Ĩ`e;BӍo&$ns{TSAnA G78Njq3Bu3%qOE.w񃗼{pR_3Y^+oQ s,{n=U~Jܿ~0]>F6RY#kashiU5nMӚKBlLYnSa׹Ug!Xg!M1<2J܁K!ll#qo;;J.OĽm J;ݴ2WY*|ýKe .Q7Cq:d.1|}"ny%$6yL >k&o˛t] N`BUT]kaz[gAjrssp=uRC/McӃZݞJʭ68J4q3M֏>otزKpl78OT >} #! QYTO#;8N%o=3;4wwQSY˟Ib[*uo&W1[N(*qv%r]q ݵԨ޷뿂ƺ)ucɅq%X׷̃9 iYj $Q'>L6bl?Ʈ9qowIܚh5n!q{[(]IS!wA6z)cwN- )|O=Ne6|e?p$w@-#n=k+{brK4NR>1*7r_9IۜG^{m_)q-se37 QjeJ:h*Rn[Hܳ7 Gei4-:`QD<݈YY򆼜 byAc&]>S{-睔E?PEwL q^I{)pbݜ>Wun=͜qr%΁HKD 6r!=}0;TAm #F?\ Ueëu"ƵoxqlJKnWzj򅒴uh6*}xTK~`$n{|$p[%cukF(/UB;|WM35ə꼯.R<] ?ǀLLc%ٷ(פj@!=Du7  tQH޿?IYDX,}sdI{TMD~zx),Χ۷MdVͷ %ښ/%&q{[㸳!JS5x. u:]Syy*̄ٻK%qE^mK{OEӥJg2{㢉-UZ4N}tJ\fT._bǏ%b-w$?%n Jܺt"Iu.׮#h mĽ%jrC$nF Y #N֥źXl+-{7%nAGӄbgjtօ5CQ[P4_y:K<rϩLTz8s@)qO6L/Hĭk%շT=Mwؾt[z85THB>$ֽqS~ny1W>.aTd6`l94Z[#co%FDܔ0!uZ{r qkPUn䟟ӑ_bV^2%Ϙ{]iF<+yJw]$nEL@Iܓ1S![WϛX֞8q2}mI G1IWG(uoL"nݳ%l!{NqZ\>NZGcECUGI^1ޱ83\|;ONev15وwߵ~5YJW۔E9u]*{-߅]M+Pvg7v)R_MZ>c)i8M}jUX{ȽڌmAT{ZHi̩*wWE)X3}[ lêY$oEm d=l3|(q ?3 hXO5وSD\2D y2SQ{Kb>gJӥ<Բ:UI'ML^]ĭI0NٽD"n!qo:7dS>@0t*75 2B-,4 ŗ{=,$nAܺ#.{7?X0P}o iՑ 73f<|?[ "dQKt5SYyQo]ENVSђL}Y/oۜ 2UYӝ*1wS鏓z? q]5w4AGֽoc[YD^$(uunJܪ TH}t Ԗ-_Fi{$q&0Ym1L %xaD-e;i'7=YosN|{֦gbPD<tWTyԈz) w2hfJϱ,o}M}t]^cў~S"762It)(-I8M;IaӄuXe =Z4דw͂ (kk cc$q1 \G7-f u$m ,BU~pqh %ST"V3Q>EǙjʀiOeTSt3!**N*6EZE\N S_VTJl=WKV{B*kv?'}}ݿ4N[ Q̈́1n&.mA ֥S˽ߓtoGk%nA"jl;{)cx/xaFr9MlVB-0JܟA.O-oC[i2xAL4 +3yRߧ~0=}|15\E,q"SY5س*GOCUKr|\9@_ ')qꬂZ0M{5n;H"bov)_ĭ}{(B=Lg@S!mŠ(Ϡ4Y RMT^EU⊐뼺`>L)icf"۰xS_Pe?~0}1&BgU˸M0Ȉс tXv:_ٻ dhvh-%n*7eO?Hbtwj8N2}BZ/ι q[.O9Dikn cL }:撸RU~FLfbtT3]򛌔彮r|YDǖGE*oY&J7&zJ%[FI;cꯥ5=K~m[.q0 nmqm{a$}\ O'PF ;ΰz#qC$na.{jJa^e쌄φ:դE;Ge%=-߶Fv6>_>{]ۦnv0)M|d[m`K{9py/%}T,%a/:7{߲?AmСѦO`S:W'v i϶?=|T@A5su}SUNl[g@rYl Oؿc[kfNBVF<0ܶ=NVaSX&W5d?jN:yWߴ>HEyYBhY[oXE{Soə)Îi5WBm@4mT4 oqˊ?1ct0͟j'ty:WPm)- -J$nAꞚ+`i&D@C_7вﰉ^Զi qHJܴ*C{nzWE?)EU3j`. w;u%ٿ$:T_v[TEftv61T]u$UWiUc駜jrU*\k"Ά`L9=1AZ u9;Rr p՗ÈNXL}?KVyC{{Jܿ~Zr3p590o[EѶLl6lo LFܽ}_`OCm /樂{E2.>DZM_hw03ʭ]\5ۄ3{p#..kFOC|vnᢟ6b<"v?td?[U¹XK^qW=3Jr8ۃ0zN{ppd-|d$ ZS9)ۀ (ldallin)Fޓ8`n3Nxz O.;#n~RcК~uO#ђ~YєX _GMT=@WuZ o)z.7HK<4E w{oc)#nNC;ɻ3ڟ&' o{Y '!< :XgU= i{|Wpba{P~ y{'hd9nxm{zTO);ޔm@m`jm`E7[tH C]/xvDz>!rDhqN77}_ÌЭe,8>ZsbH'2%u9PWE)hm(ܤ$n\ቛr Q (lfuwvMNFRIx|ً›Ƚ⁼+.}4=B $?;qw+")FQTO& iSdtC57=mm'Q{핉$T dP 欋h^(8+4'Yj\Qx͊Fkqw?bvi/w0L| ۃž$nW5YF%OQ>8ihȵ$n%q+[m@ޫ60qvtT,>Q|7%)m0Za(菂^xvٔKErcmB5Z~HwF )]bEo)y]9hJCwu"KnpM *&/Z)Nc)NDU7AZEÈ9s.;qi;6#zλ@:gPP.:ST mTþWmy+DeeɈ{ü2)CPd]t]nѺ!ǖ.s@9 Pg: ?XJ TbÃۀf`*[ƴ%ww{sib&F0\r#ї>J'*/\(jXsd^AbϘ#+ N%w,}]^[hAJm@~m`2m[tB*`KXn[(VU)Fp#zFZ?܆a4kH5iHF18Nbo@t}xC049'7)%pj=׼HeW0Ppyѕp*Q1*w8^IF&UJVJ _`,Ĕm&#BبӑEpV.\/㺱`pG\vކlG7 U`p #)Q4bxR {9+(Q$'ox7Awe: bЙ}$hLDwjj8*oX& #5sxJ#gm!k&+n!+pjιSu;ho3=Fm J;)ۀ dbT#I %|0B$YpT {?ہpch;õ05TgC\(UPv& :}-IHJַS{=e]|n91 wmQztwF%~|Bpwnz*;"[g|5զ?cKqt5V \႟&Z#ᕑ2uFܿdȿƮ,rVedčI6iozDs[O]"!J^Si}-(3DG7z3C0u;MDw=Ir.nQ3Zn:Shg$JӍɽPsE Pqu{FYGv]}qn=0Dx07ү:[ƌV!d B-7p?&DeN2Zr;(۱ 60q }#-;sC[oeҵ.νy t$4Y$іAKLquk -{{3N I烖D_mP}׭Q>b³q)Th%iBBJ̗ks8ɘ onԜO4Zp-8e{p>7ބ9FO1"Q.+oePeeɈ #45O*ɸguRF]?}6X_]J?6$Gh!)ۙ/[{73j{;ZM} $q5wQw^804E9CD15<ڀHmߊTqWENbkr^^q!ᩦ=٘>L^m@>6 qYmϸ!t.4_h$MV֫p|no>k$l{nۢ-K{$8Mu9%urˮ퀪8K_0E9#< UGIMep;* (TdƲܯoFv3|f nn^hAgFqa=CY}]1Cu&]ImN I'q:\Z\@kLNZP~MTs} /";F"~(@2PCX-ܳ7׺W"^oq*r}Q̮zAQ&F (%nL&UePUkQrh-8M>oKW"^DW)uSҮ[SڎwZ' Jפ$nǝIܯm`$FA4Lˊ֕;pvkܗH}fNS}#b1׹}Ftu~}uߣ8N{-`?{q uҳӕĭA[m@E$!t,}Cy 7ٌc rHB=-H;Ƃ۱'׹(!jt"H[Hۂ-P7׶J>oy 񞳄Vni9&IIAH}KMHڧLW"ʂ*?eF"$`"z3"wIu{ZlSPާT)ۀ (ۀ ($|PjOD#HBU+-FB[@C-H@[ЉJ=%n!e m/ QijrvBnO#6J!}5BC #؅^{I%i[ 0I!Ҷ ~?E;֋juøR: %q+gJ Gm@6qOZ ^Il斫ӖkE'1+cW]6Q*2%Uyv-zv'}}$n!u׋$1x-+YjBE8X wv}q.۩Zn #y ôܲFTą>y^.= MĭeUtfLmkS%ظH'[nhI8-BMWHaѦp~|.qݶָa_!u c4qo=+ӄ^vْk(5A)mkRU.{N-4hE.6BHB71n>#%wGRU^g}өN5!@OOo^5ע]-<3Z!#u+`N-F o4 Rp]*H[&yQQ׹-B~ t(`UtRzɂM0 Ͻw Hմ(-`M'i 5]+93֒ĝCJ%qyey?t)%`ǿC}mtY.y/}NqOokuxꯄQ:4q&IFXߎ0Y"w$|N!q֥ܜa7m%i2#5A܂yK_!q7 P.H5ix\rќQ3;DBN>N+kX+I"|&<5n*I{)HCGƳZIJ"Mi5KP_UTX[[PST]R[ZWs}}wR]I _zi7e;:$p=D'"71̦*ZaM=6Gi=]35VJd-$muy U-$jAut*$;d!mkZv>Oj)II7G nL ׬f}vӄĝx}mJ ~x E'eZe k`}CKm12^C(xt E/<*h,?sx F}}yW$vw/KzME̵\E$$Q5yᏈ1[.I1‹v#A7;,]Rxpr$o$ k2]}*-T!G$G_b}*8-rBU P.v` ݂H-4P;L[^=>nE*KE (I#wM}HY.50zF({tO΢&"B/cZWah-:uP~] aVS.YS !)#yS>cTO/H-ockA\Vly6ݢRv+cMQ0F)G%i\%,pDb[rr$~-ˣl!1@iVHhI{3 ~A7A^or>5T8eY>yUp)}yz}ylu(ECeÞJO ?ᶯG[a]<8eMj_ -vқqQ3#o iaKpJ쟦w(C} tgDE_Ws?v)sq[W%sD̨ah扊NMkxzNcϦ *uCi9c P$;RiaZxrҲr!q_qbNZeF_o1x,\ ؀?֕A^p2u /$T7Yj"aoUMgP~/O98qM:Dm27/ɝgb6GO>u*?Δ{ K/c} UtxM y8=_Rq V8%\2糋4QuÒ"qs?eЫZ$t{i!*b}[Htuj"ɢ\]zL;(ɓL}e:GOn"b a ܝ ۆS@HYe='ˤ_^L{:CE4bW>Û˶4ԛW,Y^ `]2;"41PD; NuiJ6vU m$&Q P=ཇ8Tg])aS}|>g\[vމXnA: 1A?"I]v\Q(a"v׳˸/1&wm7R.Zv>UY^zFH6@EnHdir:!! E9p`t2)m1ި,|qB E0 NQ~B |=շ20#m;@oޮL;w? I14o:}`gD@P W}m?O纮$T7n{}e%=cZ*h)iw,Xt>n)vűƳqs]â܌%޽yލqn^ \{OӒez YIqO S s$ŹqeYqU%D^\uitNgqeqq9ciE{d@>_,?((,*.PSX[>TW\_NTĵ4T>Gs}%- UL3L2XT< k:[j_Fkm\W[ cƟi[{Zz^`wk@W8j=~XB3a!y+o5!񟈿(@&#_h$ 4 >@c% @_@ F2g jZRߢ,H#x=С:xxJʼnDZ:)A)\uW=>zE҉aMYҙ C`Xl-6# YKa'#]DF:y.#m0x#0pЛH) 䆣]$z|]6E%SѓZ:UitIG Md>.LA"`8풧*U4KEgNZJOo_um5 O”h(|$ߑRd'Bq}4f'(JҌ?-)(JAAsIZJs6O( eϐzIwbqiD{⸻ !7ٱ5E Nw[r@#Nx; D'!-/#3q!2 򲇀8'q7'ׁiGL/eK+TcbB}a~.W!_SnayyYQ5>;U; =!t߃K8AP^  ǽ />BxB< o7S_AD"OJ CtK @8Μ RgOc bGq!f +ܸt7/ ]81x&pdK8Qs 'F: OÃqOEA^΋-;ﰆwE oE2g |\.:!.v^Zsx9- |W1> ˹feD~y-CTѡ{_>zYWcܸd <x^e Fe IT;m 6",s*4nbpzu;n`65n?p'a;~+IBUdv܈Ų7Du= uA.k v4/ȸVs w.BQx P|Q̸YQ|w3?"~8@!~^FmuǟGh<fs9άs(A @hT&!qO 5in e}yX7ޢX7:} bpN  p2!d9A<NNdN@ ̵4dtG` :54nf,8srkI1 >oH'{X~-k #le8#=>q0=RGB[2kbka:>[e9{#u8L5q{ =,D}{O~Ƿc/K~O K>C!8 Y mqcz{ᪿ:$h}{hN AKr8Quߛ/'"dZL룓h'їtE:gAgxOJ#'֎ Ƚ'qǘF`z$@ 9!P }8zQ"(@1AiI~$0_C$oUx | ӱp2c*pFa#J Ih&j#^otCAp1" Ľ2%HDz H d e/˻-^ok =uJә_QGccyHzgۈ @ d\|^PDLp<9Q-w1?BlW>lK2x8  1WI s 7n.‰+b, 'Cl|D3 'c8DBS $ğž}@Q z! &4FWeX z$, K^ eQ:s0`?ȼU`fGwa7ard;',ѡ0T0܊a|zx_RKAI|חd4m;{2Ҿu/D8(.8iت*X&n-ԡs6w΁[e/=MH؉SPD"`E93HW}ٌR1]1v}R- Fq-$zk#kz>tqgD ${ RUǭtIJZ#^.خcS:T]"UO^FtRK_VE wT]Hoky 䒸%)-PWxMeVݩOvpEHš\!">m=觼CjoGHCm;2Ԓ2Q6\ *Lm$9t= @C_?sF%7$Q-Gw7;H׻B99Η r1:RN>^Y:gHpG-x%g5J)yR& Iű4 +X+]Q8:@ ,$91@,{{);G!4Nbs@<6^'Rf$։Hp"JqB ƫxF3˴B"87?[Gߟe'UY:( pIjH2/Y je$.ҽ u:azL?eeO u=ZgpV{%Xj rE4u( q^ QrxkH 6cjOx>M08ԶI0FvK&CKQզQsBR+]ieW?ZQSHo d0{H"2 H#UO q]D%SMFA͈zy&~2iV;Z/0 u=|&<6/~vTku޼*|xޗi/}!?G ;'yb|ˠ u'IM ?M7ѽ?ζ?_śҹ? 8 ئ=ՓWdo_aBU?9[{QS%/T.BL݉R##NDyk}9d&uoⲉ.Luzp)Amm$nƿ 7N(-.!A@\#^PL-GIJύq7 zRnm:^)%wᚷ6ZDv7܄851/cH݅P5G0` n+H֨l T3VE3TP]r -vY9.UtRB~)3AFFfa^٣Si~e+ɛt~etrqT[I+\~'T ϲ!Y34Σ&Iai Z?පtreIwf<3[y 10 \Nrqݛ{ 49 v9 ꌪ.;-Zn%Z32΂IqH#GIzΘ*.*/Xv3X][h^I7su'/7kRe)TWR"ugvjk0W] TաnfrVh#a[m5nVXMTBM0S9 KQt[`|D+LCJ+Jsth7a6TCI>6,`kvL/-|v,U`vB>Rw_{TNwIc'j\ UhCeB9ޖh=AEXpd9ykaqϖn,;)ܹ\!&-qDIp_w.VNj<% xrF!~nbiqiɄqsd5)<̩\&|2m -',799bZKjX.a]idg,(X7Ig"$|h<&x RO+DIvbm\q0U'bOs(\wNz=8ٓݚx~BȮc&Xl8/'DIY4GEzJbPhvda6oQ#PmHl("v#}RM]}˖m!!mXAү$'(m? 9P͊ۗD4'R "X)ks\v׆(5nԍghRF^ u991dɓ(}ߧhU(R>MiHС (BS@܈GAi=[Rh5]5I?uokH5DuUg.ꛠ0姍v\7$i"|"/BW7 DbEWZFh!;iJNciT}[z1*a 7}mh70opl3e5=n| _`׼-}-Bؖ$mM)kl3[h|%k24ZfId0KGc>qƆ >sF{6&6\ey ?@ z[~3mb~ڄ鲮Y^~  ku"o9 {T_ܧ%9AFgmumַ Z&ZFڛfWE>m v|]?=`B]= `{>Ո`Lx<>߇EK|sM^D!46_m; s"]9qMi58 ڛg]#81ty]G<&dq_`c.΃=g ]*AzkK#tvPjQ.$1h^-Bu6 q ǚAٻxX q0FZc0:RX* a+PYHed0ܷ`%ŬE`⼀M03 ݼus tv,6Ŗ2]m펲Z9TW~$!*߾$.`=#}zPGwݷ -h/)5krUĿu$o f:=ihafl*%ދkM4&=,Z7\cN_Wsi"˳$6IBom%b QZQnMm2' TWXyőJV" h+Fm ,Bu$m]%EZ ;|5ZH?@m!U4h:h(yMӫ_/q랎ӔgQt\B=<'1bl &gxu|,XS:'M8)2&I d'}((8mU|)-`ex^3p/8'ҙp2ho81mf0vo`w}v9 &٦%Ծ`ok;|8.=aon9Q9wd(XW!DAN( 71NT 3usX>.(P3#-`,hz2f3by11n7% __@Lc:0f6z0֛C1֧>'`sF_az4`_eZ㠹KN~pd QG|W-֑JuOc$$F(XEY.OS+ yΘy`1DB86qדC/31{/ ygw;vx)5" }VL~ë??cBeɯ%<aӍ.{02҉|Ɵ>괖^4B6Q$DÙ6U>]O0 ý+9 m:I\o9y$(_$O%X:{_I/٬t.؟]D齂DžT瞦f9m Z;Hd]`4HfRÏTuLm 0K1Äe9z-`{״8A) ̇~N߈czRzNv vE^ QuF4 v?)Ξes`p@G8Z4ãp:tM/Cq9GЫgK᡹ ZMoG^ Hu fcp:zUEk1܎Ý=uVC{4,Dc}.GK"W yN@˾ijzxZ=>knWQG-|gN.D?qdT؞_ndpⱀ 6o%+((:p#Xϣ+~dz84q,8z;$-c}t7E}%yΉiTM/q;gp1(T >b8xIJo*ږ~#W~g7qaZWu;>%wⶪ!M  H;~i3VG:~#$;)bĉI s"d p6DDP ܷU #Ý(FĭF*N- ۓ@?&QiS0g'SL&uWz=Ex=HD6SZyr@J^\Gh>0t0}1hPvNSRN]ǭ_a^5tpDl+fKb X%y* ѲOQv4:1-nL{G:'\X{T0Ye8MFBGGyE_% ֝I) ^$a[Jk()S ڊV qRoRh8XWm7InPd !*G/5>?2OQ~kn^mDX+3 2\o|#t7-!j?cG 0nk<1߇`=68igݎ j ]̃-"rNX0/c<qSiWBhK,Ohz=[D7j7¬ANb+؉0]mvb/NC~ J?h3qVEzp뽌W%%A<7yN٩"GDre*TߓvYl" Tǁnbr8Èt:(gu⨄H'u?/X|G"h |NϟUC!qN+p: ⼋K${ qoN{h!]Q 8]g.p/N{i6boV1aFHIQ [\>c5@Kr$nn =5l H⬂8q^ @_GY`=4ܻs> \@gΆsFCY_J:MPFc_ޅ\+ע%9qP5KKd%>`\ƜѸ";xp#q'7q-W#p#nGV nAw?g ģX<<?b ǐp9.ǽ 8/# <qH$IqaHzI")5Rnɵ < (/?"i^Fʍ0 )ZdGģ+|<HRn2?=V2G=G *I Xcuz% O_ y"R_fP~ /a$/JᵓH` qHyt=y=Vs<{qQ:s13ZB eȲ'^eو%X+!/AOqy|{qb\f 潬kFIb]>~4@@ w܍xaN9al <'8+A3/Ƨ;Իr'OoG.7ĹqwA|G"#"#X/sW䵈wa}w?u8$1?[ nl8mQC&CUGc&)zxþw BeKB(Ǟ ؟xpl"\b6qheGg9WG%?خpq;6s~q9$&c(mGh=>t ɿ \cΉTTw@_Vz2CўtgEI0P|TH4_t=CŗPt˅?4ڎMBMb/譺xa3np nӣa:Z:p pROzneҶ :}yB菨r}];.jc, X֏rAA6q9} Szi7ˈ?N(qӡbQ`ǂqqeg4QvZ \whI:/n1ӂ%$3Mg*+%2giݙiyRsj8gGJ{DG(hfO;#r9w R[!7e!6 9~d %yO'Di>ES%䥲 =E=.~h4IE̯+d~Ņ5=T\~N2r#W _ /<A_yV" "+߅!|֙ ycm웯Bg=%ijl٢=0\Ѷv/ϼeK<)_ )A2c α-@ku Va( H=Qj Aqa ARMM³_ 쌹$Q*<+҂t %b+/B1hb*1`e?FYqJy?CEI&PJ3QV,T<_V^<4kK3,"!r<^CI.ˑP5Ź)aqՕ壞߱\W(cY*rQW/2OB U砒y TeKVzVH*|"4Ք-uehm@[cq,ΉkŨ,D-_W""5i꘶hjDss5ZZ^@*ZDqU\B#CL'[__F%iK4h#j XS &_B}5Q2GB=ewY愨bylKҷ<**beO~+8P<.^/$(I@6r\1{ (yEϞ0|F|ޫ㖸OS@N|*ĵ2GxQ<(M嘔q9f:Q"YO81231 Rُ(\%^p]ͥiC0׻{qK$+>ʅ`IYb(iG`078C>a.Hqw&@!v-3qb|Hb:-]hN !:J2mTA_WBA,)wR]hMU{#ZKfg< ;YUA"#4ξK[(mfa%{D"2#EBh/2?'B,݄7~h`) -)t5Jƌ;*,趴$]wѕ ʠQc4ƣQU7Y BFz[,IhϧlVU oۜRAjY!J:+xF5bELKr8糳8[BY3#q \]bb FDrho,E{S)ڈu0+8HB]y&96b(BcIIÊ&FҵZF"}M4p jk(3;x_GC!: Q-h*KE3Z4:IehbQ$)eL ,l"/m̛eݴ4I&$zr?߿h>a eekPp?X,oSnF |q#5s~h"16zb]9 hjZNd$sJlSH''70&QAm$#\Ach"qJ $ƊQ4U⛕hd*g$Z19)Fy\P<㤕iqv }91}BVv;Z*G;IjEw{z:_gV:絆QtӋ{zy vWoG ֱt\#*Rx'hoDGk5Yv ;nU?8^Vľ<6hm.c9*y_mmo)9ho.u1y{7pQOԱ?G-h5j%ly\AlD!&5V[o9[qn#zX݉I۲@XL8G''ݬ^%@z! ӊ{ZٟGoG!Q}Flsj氎CQ$k:  X:)&03KH0^Cy Fz>J* B]Ȱ+]ڣ/)䜎:i k\fПZ_Ʒ֯6&N 7H ]Fڗ0hѸ; ǯFiC1\Nޓ {aT7swU¹@.<$F'K6om wVNc.TkRu,$qOܭ v̕I'̹ϛHBUi }܃*MEьb/h"υs >*m?uKoj W_$44̓&__r3g_1wq(+$qCD!3g$C$<P$]CR|{5ɹ9ddINI 4QPEm!92LxdH♄Z U%b9L\# 0A!]yW% Fι::eL5r:JT@(M(گ?N]ihc Gu~i3 kCtrjsFɽ0]ԭ:;߶z dzMn8mRM NdX!+\"M5ǩbpTE;Neݔ_撴gHs1#)xĖ[Wt߹}_9:A/`8hsh#n`d*ǷIƉx0rNB3vrD!TY)ugn\;CT@ׯ&p-}ل\#&9cNMEq)Ib$<%)ej ިQTI xPoիg%7 *} ŷ(.ģXؗNUѶBf{ޡcsvvf\! ݅ _(-J% ,6x[^sRRz acʩAɩCa*YlK{ɻ*W54)%R=_#Z=:[Q {((U*ʲ[6x{۠TEȠTm}[QjAF5K1QchQBQ(e(xt:zhE=э7:ϰX͘&ΩfEb`MisWPHQoP]|B]%,R~vEòqDɿt> ]6ݗKHu­r5+c .'wX/e$A Lr Ǽnaj(dƘGuݍ.#`݆$La઩cж+>p8/ǯ"AJ(Nr릢M$8eiLQc(L6!y)y7bm3EӐR6͐mI wbIf)"r$DI$: P,IqoɵoéoI{BW*IqRHYgʎ܇ɰyy43Y;]1Ug,*WecYw_^I2knWnjN$qF ;DSMwܛ9tѣo@59g$v n\(c.*/gP'Y֚$mO4KϢ1IS)g,j»؜$a`Y 9 , K꼻䴦L]pr\$sSrdg<'gԤSTQ'Z* ]H:F]j ƍ9Iu$} iiCïM Bܨu 4#fN odςxȾ@xHv{+Sr뀢.yڠ}ι]^ dr\lӾ}òuփy4߇Ԛ['ig_Ⱥw8MMa~mhzVRnuP@J-S}8l ilv (Y7)tg(1̏LlȜ7f,;(D|*BsxQvy  sS60*1Ybxcxa@@GsZ y@6+%,WA‚W1젟#ar =pO jF@g 7p療BJo(Xx[pg/=dM] 3w(y S3>an sxђ1ߛ;h Lͤv!H6ۦR]'ݥ&xGhs/{x?]0}wcg|;)v G,~ooveWA~f=B'od 8&VI6$Q,c*#)Uٷ7JF2}xgKzڻ;$k_cB$Y5)ى%x3*<(۩BZz8B[a+mܪs&8sZN*,ƔMF;RBsz`V*H3#.Hմ`qC\[~⚄`/W&jic&W᝴ʏ,5ot͒v7Od&`Ap}' cC 'rYZrqxߝT|Ӊ]i,Ib#Ħe||Brrݒ0)?l'k##~F,83_NI]KIB{q$_y26C}đ{|8r/&Jlrq[|Ы46qh-HA@)W:=1PG8mm wT|&_`~^#yd?!4-Sy34}暽ǰy _V~8ii}zl`]c6i?hm4zmiM;lfݠ1/G|*޾⭱DzX֓[Y&狢۴{7t Wg#}dz{b]k6syH>- 62eflcs:Υ6uܿڷ:k|[pb|gWG~5yWy5W}Ybm O9F￝?^[oD\+'J[y5?j5WimKEM)ɇz;{ߕZr+q< YR[V6Fu ~b)<\&?uQVLņ+a>5<$/|OQhߖ%KQ>-Y?mYwd%ˬ{X=lxaжu6k_(q|}gq,ݶpVAo@ߐ7}Ú(ߓ5k^q|aRξwcl^~[_KVܚDJ=#nWVaMqw% b%Rb!)sn7uy J,oW/w1zծҗ4"":"of=}Pۡ1e:;ET !0=fٹ֩ԃxgY勾y[aR߾e=9shNU? 鄂;o;PӉXU<,)ZˣJՏŌg3%ft5,5Uq#Ií?m/#ԥGp'nF迒cc~D@}F.o|nr}q>w}9/ |.}2>e9ge-wAEjYy_#m[3+^e?mXlVQe5ESR((-ʢrO^ /zxǀ^w|ޣ3;mEr(ort܎)*cK) (Qm#VٷbVS QOѢ6Vc豣,pE(T`C`ގ%Tde)UږrKcS9@EyOiu9Qn<-xJp[D6Kf^cY[#mRYn0,yGO}łE|_obXK,,Gx*Xҷy|_1,y˲i;g_~MVܤecފt3C5|¾V ]#nwe,ZSlkEw9yY+c:?n9/ʇ8B8SYSͶ )"O- :#S~%s~,^K|z&AG?ZɆ5n87F##}i h:O]aVhT{j1m[w *̴3RP/j^kը~-]68F=}67O9z* 9c=k8btf!Zc4NVqa9x;V•Tz#(E-%QhSޫ*M멦5mW8J*VGkԊ QASVtT ^" e#-kE9RTx!d1;?񘑝 *4ĴC]ʽ)K7eC?P^A0!0e, ط{1w< cΣ؊@l-%l.Eϥ'>msa6}K9eCgPw_ר5U>x+Cw;d =ueGsZft{&egpicA Vx+c, s{Y`KlQ氮~2ѿ5|o 3LeCmf>7'@:p=E=oڹk)EJ_IFEw'$󹟹IǪ+(+>e1a(Fp}Y^d%FLU33ǭcB6EC*=6$FQ*;1Wr~flOP6B d@VCpw3f58cr<(Bʀ nU,pT#4,8n੫lδȜ{jJ>wu {;g*iO,oIi8( tO~; @EygY"ECf R47R_.g-Rt¢I{_KVt8[v#3q89%J fsY~[.9eH={Bʖw?N|-671CiMrbU6`]oLv+cm;3&֣0lOMj>P.}N{R+i~P8> 59XMě6jn[0G:l֩cȶ۠&h5٦05|pFHUGK4DXHtT+ ^ĩUbRXN-(fZ5,8LsTx  s/M([ԎI};/Lc贄5]SOlth-?h{fHb{YMjRF;qx)u#!Q[ͱr F30-!o#ԏ8n!Br!uH#F^y^y12[(o>X0ZȨAcv=. #6X *HbAp=UFc?ڋe1`ݧծ}:~ Uceʃ-Ey.S+,sJ;|~DZXl0g\%)sYi-8cZ}Mcü|//Ye33YBp֟a|aPn<Z5d0+#,\0fY/*|]$2 #Up]kQ8WCc2zm\o<#=Ps}3˭S9sF*qR@7аm/Up4!aʁLR=s+:O2^m/༷]I%W"aв m t=&zM|F܏Pr7wJJ K((*;F|('Oxe; mĭ[Pv$߄ C1u.QTqPi Q*Zcmd{^O)vmʚEmD(1lf׿+^6߲x['~]sVma+¾X>N¸]䵑|8W)kyK7 ^q fՌRV1ZYɨEk؎ĸF̼ƠmG܀Yq#kxFAP ҙojXO/|s,]iWp弘?~cX w[p8Aq GƳ/ -=ݶ۴]Il'wboLN$q y$V$B :>x5Օ|i#pxֽd94Y- pߴdK,{Q\}3oMt?Qt{B1~.3Rav2̾=OR3~ϴG?H4r~̏rayhϢdoϔ;e4;ķY]wd۷g[ߖo~[V+;z;v|;&pl[l<ތ}whum7a9m~e(;x9m~9{zwPmŖ1o6-dknڛM<e#h"ֿ Jª1̳~Q>~oǰ/}Q"P*ֽrA:^"e _(.J|o -_h 18PeI)UTPҶ~4yE/6)*sK8Re/~ M8,e}|U%I%(l+PFUI[3Y"JbDQ݀e30+gּ`~,U(()kA:e5g!oJK:jUewj XH9:@in·r7X~bUH0ȸ rL:.eyuPyX6H"Z sxsݚTM0PP ZqrS6uEa'\J& =5𙄃M5`Ȯ,j&?M4ܧW]p6|N:rI%6cS Ryd+SlzRCҕw%SrkSRymᏽ';=/Q]?~E^[{ސ۞ү,Z#Hj|x$^_Q.GKſ.{ޥڇfMu<೷Li-Tɜ\ *O\W`DDI[5.o&ms"E"K@֖L$>YPyW/~%)Bc՟&:.sÓ$_و=\8 spNn޸+{xxQe?'ZM %"mp1N=I_,*|A3?˽yYx_&E4O3/|Qt,m|38Ei3~x81|TJAKOAj"d}L')viҕgk ֵM)g8}S:~kLy-tGK. <1WA5'rxڝrpeߗ}|O(S~ Pyp֏ `?ʾ?# ڦ r0^ ?Nư a}>LUq`װme:8}g?{Ut]_$~<=|(cT@GHvbڍuBqоQ=Xv1ze)LlGPLlGSe*Ec C*HXU(KQVXَWEmm|e+PGMv  ܋6CmBT6-Q6ϲ7^? ܥ *JxSt]tns?>/3Ap!`&`Tqqj_;y#Rp]c(,C>Tp{ב _MCܬssTC sTpgE8_Q! `xm'{ \<ە T{ls}+\G:uM49M sNM( Cz;rx5a6 d$@& eed\Q%wUtlH~se pLqE7 պKV.繬T<W%08.{~S= I򓾒/7f$w/ޛk*C" ? s(gqICMEEΨҀbpX.rJ*#QؾH{ȵ2TM&tʄ;,SK^Hi8qq{%Zw'򊮟DSXqNモYQ>zGm?{M_s',}F)K"oJN'RM"V8c8|ZPRN{ LTQJ (HT>k~v,kԥnkO(HF;$ Xƣ4'M |:e.KVe9c '>z%yIpR38]wO_I<<79EMP 9V(M jwt>wt(XcQ 5<8}X'ا$a}RNsw̢{=KYseHf=e= I#0JRB${{Y_Nm9 >"e"L WCf?g#QX"K= [H|T&ʔk$t9j_ޛ[M]NbU F-ȽVhfN}<)F1dN]v]Zn.>E^E 0_Mj.?Ըg$aM>V?+ޒT*%ogčN?UvuS7b_{[x e84*Md/h>*=t'#}H9l{RJX6\\ DRZ6}L=ϵx*jg9׺e UK]7xy,'9^h))d 4SUf*#~J%hFGg+߭-/$/ (DĤ!:Ch:-s +(繛_f+Vj>麾A&39P_^9(YGF(4221÷(I*:F- UdL &*LZWR\ʪ{].Ci~|.tg`HN,G*C@{0J؟W4mjQ<1wx6i>!x`U` S& ucZ+8D4b%x)ĂDZ^}utO%GNo|0z7`i2ygp͙Ғlz#vǢm_3{!@3b&jO NHϴsqow{k0C)v1GL=::uߓCJw P*_?zK? 5x+Qhy&5?Og`s||s>olyP'<8ٮ}p\ݾdy2JY&JNJ&/t]$FJ sϪ "yQtnhIbQw฾荡v=g^`QEGo,hG*5a¼O st2tI!Hn^\30|N/&'Ek:Үó= }ObDSu Y? LT']$@QLm _O"J 4L!SGaG=3ˠ]X:NOvތ!j=L¹GbȡSϓ2=FYb:$g1h>J{==ƴSIۤ)ch=1(@)kO3ZJNAFH84ΫQPàTPBTP/Qa4 DKi/gTREH_e|]^sR@xNE{mm/(;]Ib (idbZ+(]$^/r]׶X*~8{^ymʸ*ƀ"RMDB+[?_ñ6#Q|>rtY,y(YFQTٹru독*F9*|o8 ZEJ*,K1T1Lr(SrGjq*܆߄yz8?zG$P,dq%@96kQ?x'%Y{ޔѮ}W2f~dc<@ou f|. Q`ƐPJ}7e9C|%)|:rە#2PT3)]LJ_w$&myjbJ_MtT QPT)| t剿 {Co"pB9oYYOs*K֕@966+VT I ̲9%T`i $K41t:VxLSp(ހRSOPK(eS)_Veꗹ|~]QtEKKSmTk:CO?$AcE)0xЄ6>>%/ʑeɋAqA9xQ|8A埼 ]FA>ozxZZQq__xr<S>/orWJ^_!qPOc4ru 1#DlY6m8oSUW9ύ@沱\!E"0tdRp߀Bt1mIORq.4QغmHAدPsv[<2ҕ/!_-͜դxp-7I@z(Dܕ(6lW%( ?~{.qKdT dX|< ;nHx%c eu>ҏ1=Md(@]%iqI((,X(Z%AAQZMCGscAAjk~r\ PN4snk+}55F 5:XSbx (EhJϡ6-!z,FcGWĠ ŤRu(>{[KB!m/XKA<,CbC6?UL=7cJ U 2cʇ2Seě Vڊdŋᣏmf8a(2BBm#0f3R-/.(8 3uf c\MjݾrIrf9񑌀.6b_?J_\1sDKY.,!ЁNItލJއBЇҢ&ӆ`@9tty]8ۊІB4L,DtHtTp.(Qpͦ~(J F3uha]ipf`iv={BӍŧscʭV2H>N+R*u9O6?Wu2K5^lWBR:!xM/:yve뢌`XH!# ȼwHBjy9ӓ(*mX@Uxy0?-,wy/ j$GpU#* 03ݏ c*Fu0|uJ|)u y>S9uԝ9:%-'r%m_?tOX e0A< ۑ`cdzglS\@H4#BnG5_Q%3z, wb"^/ʉW X(Lܲ@PC((#0Pl ƂeA $BQnn3RBY`AJ]&!TPź JRe)8R.A"C?P([- e[X|=M'UT `Άte/EtdˍDž?hmiz^a14+Xt h / E!X0Sf(xy<Q@&T{Xժ<8TAy` @?A;U{%TTzYW1XNvm1c(LBcڄIE[6-&`nn-M)PI@Dz⧖O=s6qE'oܼTX:X.Ԋcϋ;ƏE˶ˆZ,X9<`p"bc,BYDɸbp Ewaa$yڋ 1OlI)8euě(i3DJ&~X)>,X)ƀWۡÄ>^˵Js+.><77jFQ+ n8bNܾir6<+I,cL 9,ؔ` 7>p5]w^epc/ @~x}3bda_黀S?`JBg ghxy} +!kWԐ0P`qخGY)aMsY,$uu"=$ri*G yC?Bp7░?O 2ɀ#.:Y!Ǻ?}#u=FXC Cv Ԏ@oQB5A!!0HDYV"GbCXH:D$p> R2DC\AtRF9V, |Vm*ja0aӆҠ,"q<2cZ;P$(0ʂ RARL[c&r,\#uz},*OyH{a!>C+c66aX@ r:* 5qXg_)AoP:n 7 zbp3U*4a- `2UPQbg^ŒE\ a^4cd2..`<XEx(γp$ԑHi|LBX(LɄu:QBq, sg%&R 7n-X-Tp>u!Rs Jp5XP>!3RJkDXBj0}wB(-Js٧D8+hXBB)>ƂkͰu8ӷ4eQB|(!S-k=1+f:ҧ0fR= K ]dt1l87 l>=cC7\ӫ?( *w|o2> U(bQȰu (zm((?!%qӛr#E*kbIzȏcHg㐙>3X1MӷC7ĭVP,/>2YwW60 Th|p~nF6~{~y(`LjŢj9I>F8&Z+)FuXF5hS%\#>4M 7Inf)Rp.3>v:dA H !/[#PH_Ƽ1ueH-1#==.Vȝ~F>E A.wx]{9QzWFx3e]zk%#h WFn Qeas07 aFܻ&~AI>;tG9}cc0} ^&vY @q犽 {BYeB N R$N!Fɰ =J6ol1԰~;;8_H|Lxƀ3/@(S1B77EQGo`)\`$5q3*!ܕ%g!WB7#n|8l,*!{q>L{ {t HcĀM*a8/i8p (F@(꫁F%+ D8ʅOH cUUTJA#e!J VXX"ʙ(cC,x0vHr' X"bb8p %G@ lb(bFF` a`_& (*) WJ@2_Y*>Fh¶_0^\$L1Mex0wDNFq`\-2So|;e8(mOnP~*I=S)vb^>4O2?^ʼn<>a `t{FT⧊8@u Bl?K}pOp&ddk$Em/| BOsCu!0321216`7`C510LCc4B %CX}F!:>p4~ A?S}h5AS;QJ F^m D(˺3r3bW|{ K|LnlwN.LqF\9Tmz/zՕUJY3m>^Bu=!0Ia›}z,:z.k(~Q&OxQB|:u´*@DZ[yOQrFPXz|Xr 3B;N/,kRF;$Cg2WE2o)ЈlN d:1d*#~,s)F/>K2Fץ5w#wG>ACpu3%B)eNQ}Cs1g>5 wKb=RIqNB3>6GiQ@\xR7)dRLRHٙRra]'B?Ԧu;w>fo‘ÑPb0؆vR.^oC#y@ZЍƳ\!}b~eAJc>W!?!qOA`6&pA2::j:RVK) g5k1c!P+A3Q|\u:ѡ\(X7k9yuơN8m<~Ѿ9>MLSsm:tq~8[5A_t\c,fGa~$CV~hxaW6j-9p5'QFH;D}#D3Z %|y'ۂyfn%f-qFa|Cy5 ﷠q^G`,^=fύǿX<9xyĢ(8"ws#]B;3`Mg wb㇀zct'Qc: 4Xw?s>}Z kd?`~}}x1{*q DxUv0Wau!t݌<{5 +^@ 2 LU#)^{uD#T&— ^˵(aje's7~5ōd=$ f0.o!? yǥ #BQZ=qM5"jj b Q9wn= u+)Y̻79oPsn)җ 1YA&܇z}8,7K-$si [9F7@TgKyRBR^JoƛIK"ܮ{=?y] 3F0{# C0Y!6/̀VH/׀+0Ey^njje9<\5X\7sF Yc斍3B=Pfyu ha?Pg4/ۋBU4Lꄆ|`yԃG=DaQ HYv,BV_ݶEb˳Flx 80#}V?䳠}} ;~F6A h6w0m;OB~>^|mf~(6a:ץ/lf@cțpp'#;6d8"KʂkmiC_єOh<EگsnC۴Eck 9(f&qP 72А\yD1ץD 썢ڮ}9W),K='R07b7p:r@-QtaN? sx[` Uc· 6$'@PR@Q8#\b%CG%  27+L8(lR@! Q|n2' uIEb#4|$ dѲ.Lc#H4a] 2?EAd}g5\@jLL}! @;N w2jGV3ץ!B`Sڟr>p BULupΥJ<+-h/ U@Pmh*J^xۄX:+PHL@9 |`9 H*'32fW J1EX Hl#|8趶8gg"nι,H5IH`A#|VQb(9*U@) yۈe;Pۧ *E 6VƢD9BD{(Üܞ^ը#-XDXEÐpa "XFpsF;&L}r3P*?awB(A,A3 l##oȘ=3?|H#SP8PQ\ɃCXWvHzlJlfYx!;[K9}Y{bs9nW̎P{eXF:-uT a1sS^!Ыz7eD ;L]?A2"-A=:C o݌ a#b(*l=m,eЋ[]#G81#lጀ#W B1 `?6mۡawmnA v# \!Ѕl`Tꀀ Y~^Fͧ rM SI[,lMCvK_6 ׆(x[ //ha݄n"'v? !CkCkVj>Ic8Gm`|m=9mj?A_鿊Й*F[|8X P^uwqh[ɘO}Os-'e J^[v~ O)s%P̼xJN`ROṮ6n7 \EmHQD"12+"#X,ŴٸY:xJs^P]}8si3BpL֊h/MJZFB9Bi V҆2;؈F 1ӠpfZWM_Mpzp%3Ӗ~!o><껣+}xbP4g=h4?CπG0?5yw@h8. G: 2|) \jJ~%7mҘCTh_)7J,*0,H@`8pwx\+˜7v70& /I]1z )yztN +.ẒPO P\Cҽ: 2ur4;dP~|5IxGWŧ-m RxX*.jQU*B?/B;?c_ fu-ʣ I{@@tPs3#9l xڽAoG3nH s :f;yη=|}d3tֻGZj{jʤnI5co#`>K_5Ȗ^4^GKwU骽Q'/es }5yߐ:m[zJ}io/6r|NC(Ǻ4C{8UD4~[rgۘ=ȀR <_ګ|Ɖ鯣 ˡƱ1XsUb""S6J(5۹LYw$ ǐMf9}:-I/#^b/#OۇUہ Qt[gmq6yj8k ·hΡKӖ{LyL@{(3d:(JC8 H_Q S J #KCGIENc1E%|巈iN':jr")Aõ 0uĉajEh [s]h~#oV iuZ<=\yyɅ|zS$|TC΂ڛ)ܠ&ҌyjG p)/S1SX#(ƅEJMS~iu\Z;:|#Ow{@p_?s_  0U;8}Ow=h@`Gz^g|Ү p/GgG#'n) 6e{2os񊻯Ak*|x @p( ]o:bj nm~sjcco쬑jRz+3Q!qmP&]mn)"K0JO# p sJ6]($ccۡ6%ʢF } l[tV_Q#\cݢ*fh*V-Z+.%(}3ġJ-?gPrP_.uEg?A=m X84i4q>ќ41b{ -%}B&럁3%?M&FH<%I)k[,l[fT57N}JmpusX6y'Qjs C] ;ĶzmN2omױ?9!84 ԛQ4e&JϬ Zy7HGqtikL:&c櫻=GBRJ˄YуS)۫ɕ᠎&Z0+$WsGVs<!:2dMA/Q:uNzYӥB">3zuI-hBr->4r(1b0F|YԲQytnQ:: o׺g=o^?bz`K]>o؝9kSOW ~d/`GF?:O=Tv8&? `h`6agzxGzH ݷϾ(]L#(!OGCݞfіpݞ-+8^v聡p{wp:0r#0|.@)M7yp.xzϻ{ιkݘPVPn۠]W2U9Y}EtՖt׵zGXz!PWMdY Cp^p`g%J+0D@~}(JOK `C!B!ju[:QPG( Ҏ +hR'Qjr%ЖkfӊpW%J96MeY(4_AD ͔XTYe'|"f @vM͠ig ]2wiK깁 `$LkѳPذQB=uNh!u c1sOv$,EBrh#IaT6pZcM LaaՃs(,Ǥ[biP$˅c(UtOыl0ԑ ɇ}M|O` NO ??I?? %&ÿaeސ(r%'`,s8K@_qsDw|ou׺: (Sa%&n85`?DoJNk` 솣a2 ݵ٨"Zd 3n`TPAx{*a''3 ٍdZ.5PkdD|%05uM8"QL4ZSk 䀝QMvRaL05j<5&"o5DI4&s"[NADE gfjpHZK gnz/S#X 5 wMyj&|;vN^e EW 2f>̜L4uz0],U7Igqi7n1gw7k9[Ct$*iBڕwrL^"Ќ㴉B(گSMs"aD68F 8yT[w)D=(U9`*vBen;qž?);071=`hj"ɱLgI3]]pZ/*❻HB@#Ddj jGL!ጾ$Y W૊v +HSq hOw%b=B@"W_R ֒/œhZo>{Z1r_&e Y' ]_ɛ| 0]-&B(\'8i :UE/72B."L!,XÃaFɳ1<#0y8<$ K=2ERV4_Ý {( ^|aOtLK-.ۗ=#>'"c_ )+ߢ|G0]^ړL H{|O~4Zefki1KO1!-d%JoqoZHd`1۔ȡ.qS7c#CwD&U֮H`o.jp\|ި?0DJ>yO`RO ;7^wCN$"޾ļwah{2IxKҞ|֓>'O>?'vjgxuձ3onJWB)?3;Ia2)L&ɏv$O  p 0=?aKҞ|֓[`&p{<&3~'ɤ0N&? xv{'=?*MIENDB`plumbum-1.8.3/docs/_static/logo7.png0000644000000000000000000002274414613634536014271 0ustar00PNG  IHDR$hNsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.100r%SIDATx^]=fյ"0aƉWAQR\.(b1]$&D %),BXX2w=g8ߙs~k}]ykﵟֳ9s@` 0艁 #A@` 0莁 Bv@` 0 ! V @w to@0m0@` 0艁 $@` 0莁 Bv@` 0 ! V @w to2s}OGFCE{@`'4dʼnܔ~#w3]_\OPpJ@` 0AH!jL;@O !iGHNH22K=E)%Ϯ=$0e !i@H$8>$# %?e`jؠ3^mLJSC>gsB" y5|-HIGdh1dGp™88<7ˎfll?Ll]lL<ϡ>(p86֔m<p!<% rAHh&Svqr([qyLm){eCV`Z5Ci E.)mp,v!%$!+@%$WG}@3B"u0(Uunww$+r%w#0x`@|Ӥ#p $$ \1#v۫´Uc%$q3I!qJXq2kb@7>َ;p- In3kn`ۥhMZꤗ $\{aO+7I`U}(߄q=HOJJh> >$2$M:3I^& E?'gI}<ݺ.G,8}T!v5^o+Bk"Z2R1!h1BRm!#$I $AHrߛחD0qw I_K[AH49[ B$}a?`ȽzɥlZ! '+8BXz!av]6_x*Kچk/嘺izS_2F<x;WEu,mAHaP;V-ʧ8b#Y<|2ZfoG ߑ#!*,gZSyJTvH_Ւ7I 0#⽚d6' Z*!INf!lAElDZ!XLLɍɃD$6j Ey'5KkHX͘[i-MH#-R?ήhZl7P>!i!ZqcrLJ=/8*m"$Ryys]|x f޿g_LD2ɄKIXy&0DŽRn%#!)%Â1<8/<>:߰&Fr"MH>-\{b&+D{ӲN<32Np pY H9}w~r]y_ }'ƥrR ]"l(KJ9_) dOPer0KJ,\?8]YDE_zS.p PS`#m»֎!BT?',چS?߂G<ώ(_0ʃܛZyE;/|Y!~І)WUkjy\I7Qw"hr%CH5U=F@vKc߁A`jfذ/ 4ٱX(GoN)pI͈sMHN-/ ̾-. i# rN 3R9U$ada6l p4'<~1J TS~(Uϐ dd6"w6LH6% ,6 xy;Ό XÁc Zߡ $9B;=Bw õ i&"ҸbS;tbFHT2BOGR~|g6q2T.}f'.ϟI-AzŮ5Q: E}ægocY+6 Co¦aߙ`:;mnc h>5٫0/8&3oLflRaeR"a3$:6cΌqS_22?'< >m\tdE_k-7d6r_Ou.ՐLu3f1d!  )%I@]Ij|L*Ȇ+lҖ@zZ奍C<+_ \Ì~de'ȥ3$soCmD|eXtQd"S:r. .Ցp@,gd6-%o1㸚Z'PK? T6Wd,3LPreȿ2*>ϐGyD-7m'oϦ *lۓ*K '7KM,+mz4󗭲EMRlYJHo7AO[u8|nR+LÌV 0sLgM&>Ʈ:f;!AvQ-Boi Gm{F_*i|]ձ\V^$128flz9? $17fߥ잜2D;{qh21ީ pI>M0|?O䅙~`SA0u_Vه%C=挛WNkoi.a}L9* %1S]f/EA4!LxD³}* q["|5Y" 4)!юӌz+ƹ%)b lAί=p2l=!s/+@$7"vǤݻfHsX!2N2r@n!BsOhHCHGIPrWjNh+kL$}aF <rc `E3i$vlۧ'ˑ f(lY!$Klds<EH\O6>`RNwqZ.fE)d͓=$FgSMH\ R4XBB|gK2 =BR Q`13{fȀlv稿Bz4K? u[[I"#6UM.EyrK=V9B¬=8kEfơ=eYg3 v.O8)Lրuc5&-jb~*q2 f \D_blfK$KU`ل*5lPq`ʜ秜/ :1+\PQG1wEpq%M"Kۗr,JdIDcMH'8X2 EfHQ9%B1t+|Z z sbI]$:DR$ f38Ӿ}ABp)՜+ qahڭ0^oBD*;{e.Hc-\dXOBb^[NiVrMRT~T_1E:]™g7RBXP4WiQ dT{bNmeLsoŔ&=& #dreyGey.6TK6*כ`نYx e`= iF6}vyol%Ŧ9c f-B¤R=_gl$= eZ!nlԸdQu!Y0\%@8! 7VˀQ13@[]WWB 88s!r٥flrewu~j'#t]d2' Գ5EH}$[t PסC,e}ۨ\EpsRqE-C+)p95MԶo2W̢<-؃͔D}2`uf/cN2/mیي~ ?ױS7d?/K]Fe%W=Qa﷠@cƍj/}*}+vh1R`}OBL.i32 (m;~YDmܻ@HM-cUbLo7̈L@`Td2{dk&" sUfr=UsG˲dO< ;C7ִ43(6u|fmu . n gxDJu}7IdYe^$V`,K Xᙑa7lBϔ͐4$mu!ap[A Kgd< U\jL٬3Wp.v?9[83xKcFbfΤUdFk `;YBdl ci?߈3rwx"x& [p 'Sp: @]ͱ}\4v2$q#X|(%1K•)iy-@2lXvyc(|96XI'T* spئ ]4|[4Cb9xL1cf2f BR|T o錥= Ⲭi{ITfL]\92F@BHQl01_`nN>7 YtiK5 w0 =̌QAH\lc#S(Ce>e ŗ,Rd&$4"$7 eaٵҀakNk44F]l Qz27M#Ǥl7(pr fO7!%jӢɞ,ʤMMlF)ƈE- I>SŢW;sMkVr rYg[Eؚa$$1x󩳑c'Dr˄ͦ;lDg=KyoP" ='R'l#Q'k: e"B"C5y/wEfmus#cbySVK' EYVbfz I{D s2e3<s>zO}!= 4o3d92\F3B&p?=B P'׫=g.RCXp3 4;-J2:h:P`rHN20Y7x-E渨'5a{2iV-贇zD3U0zCmYC_Vf6eG;^$ղ݆/ $^+&?PWf $!|L}+U 7_0'ca5$cS{9$u411{Zv$Ō*df=dDDTz0Dj C\D.⒍TOO2л.`t 3c$#qS/1,繆›^nS }M(Cf2/J;z2JHܾ#V1cԾM"} &\ ʒ 6k+CPAĥ"%AfXpe)a2k,Rqyfo $gO6K&[TX̥~Y6Ѽ)^c'=|M'l,]?`ֲiֱgֶMS]%+] kR:ltHXb2-Q`+l%..ounVbzv &+LK d{0v.[fZtK]dRX:T03:hSdྑoz>\V& #r h6.~ v?\.1Idmr嚢=&uQV}bܠ"kowY&$ig֟Sde7=IJg5i/U2H"91<2Bo߱ou~O*7ڏݫI6>Oq ZAfHR%Эe ]f= uGN1f,2#LN` زdMVSK21&C?QIJE&CS"# =\O򼖯=⿐\k;L KsむY-3"uɃ? aSu DI~Fq`>sIQ?!S~B%$tL7kJC5fk.):aY1M˘:W.S|d zwMjLgPuizF9,f[bRa}O{jHuɞ``?UDa@%W k { cl؍h"D>h5.JHMYgyy7 E<!Ll9/ %w+ $@(%΅\6Y۹O)N qu9Ǎ:lGˊl se I%E0cە]fzLrc xŶ?"y0Q=!C/O!!A˴\4X\tf( %WݱQղ֎w@<6*`qK6SH8M{림Xp e3?a}䀑1;.Nw}J&aacJ )߀NȖL֜v=$,+,J)]BF,0k!bRxXAq_Et%YAHIhA fHJ9!)㤤}wOOOIIPsm$K#{`$0-!9&BweȀ^G2Qqf &? $hjF Vc rw|ov /+~TNA}AH7t 'Ng.DF $llCvxM>a@2|K2$yȩUG!!)&t%Rے>ƻ61%$W$hx0|(}*zt]`@q&!*cehc\b\y k|$*yGg-u>$AOԨO*f)uo Ghh5tu 6t: jBk'e_|5VQO8@` 0 !l`XՅ!o(˿loގ <0!>7MmfKޕz uf.0 T $Ip* {n)} bc $ V &V ,&!̸DdL"CrbN ,a d%b*L@`AH0x ؀ZX x?[` 08< !9@B~xcc lM|"zIENDB`plumbum-1.8.3/docs/_static/logo8.png0000644000000000000000000002062714613634536014270 0ustar00PNG  IHDRf=cvsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.100r!IDATx^]?/啾E ".br# ^IXh!\,(t1)>šJP[ IX$,Y [`utyfN"lm}qfvg7HB],3F {-vKe[s*{xK*V_ Rn2Tm\88 @3"?VuO̧4!F_O/BwNi(# . iW^CS.{IBneيi{W4bSEE^D+B/Eso!t?iK7"\7VBQV3jNNC<Dϭz'r5o=]z)C@zԹkaqWj;uJ&MX_+B/Qro w^껥]Rԕ.mW`#E{-/σKa)̗ݶ4(ZgmtBk?"+B? 7W#8#0З\质A/B!Y:v)pFR0I}ߗ t-"]RvFB߄ˡ%羲B+'xG1!e8#G_>Хx\:=߼|.*O1bo3PvyH3M%tiH3wFe*}-:!B1S Kdޒ|(yk ?t;im[KSmd~b׶B!"8-[t~lLN.d߯}+_:D4iëT ?|;{I0,#Pi '3XR.s j%H{"kmҎsb5{~-R|#u0ePzx$v's0g`9EN2Aƙ]"uS"` J9U&d06Q>;.iGBIO720Mv0D{oZ$ZQx`0=K&Ǎ͇k%]ɀ y;Z 3 pͪW.uaB1\箤7(y셂0A1qKu6aء.,MүKU_. l]S;SnKIobߵft^(My[~R_$/;CϤH M$OicA1"j8!,uYBNfО)L_wϕ>\2Tq}{!u= f'rK=‚V!OIfGXPik`yRrHIq#|;pw(b1Q;/aJ] z&#GSZ܌CH{if^!.&VEj7kvu>PcO r 7ƃ0yOGLB-5ӖP|6E֋ oד!vMR{\'0:Q c23:o#CkjO47`VBQt/ %Ȕ7 N`VI=sg eI]gDx`2Cwta(OFU`$*іS!;^E=z:0Mi?c1[˺w%S6F;Um˜`iCR\y2%MtIp`e)u98U$I@a!u2f˙TBdYt tku Bלngu!$ 1R@!28lAD( t .AkiY YB=#c<E`@/d) i>zCC Ro<0xC#rsU1疙zp423EBR~aČ穛I@]B]`Huf\8Q)$a Z5'6+L$)с2_^3c1Q$Ő_x~d>ʸ􏩓=CZVmQ'YG ]&F)0y/潓E XH9@ &T |12,4exʄ= зQn5t GrR9ဥ%/cIatCYb<{o DكV%0.ZCqfsoEBg /z!WiMgLȐfq"2讥~^>;$t"91G IUToJk{Ry4+ 1ޱESBiOr*C[Й#TFN#ݏ=r&0"c>ܦn"zoS׉!ԹP'd3-Ik"F+J QuȰ:6+}_vǼ#0Id}$塓 <%EvҞOYׄkeIzCd}5k^?o!x.yg;L[ʜ0ެi&ଞИ>ksJi0'|5M6#? oX5/tKE&~+`9O m2zt "B .i7BS(\5Eɕ0Qn;\h$Qp5L3_Rw[ \3a1ciBI Cmloʳ5)j'(SIY~ Y'eJSsxXTN)^8S谡A~LJ׈úgRn #Xm $+bҘE9غ-p+=V34/)3[y<0gX=VNl7@fF)uGTx:$IjxR}bc-l4d9FBWg`Pc ^MhP|:rCϴ1:յpХ9k{($"I{fn𴪐>QF 0N꽐A cN 1G u]b\;Y9=6cωaU~ؓˮ 3Iku81}:VME=2d"%` d焱++Q.FjG>5d\p/&B:b"|̐r0R8*iޕdikx0/XӠcO GOg$DkЛ'(=[ kc%oJy:|>'"xA>a2_T5e !umNQ!# ,HwX1c\añ^PEw䩑9E-,~5t!'1RH( ,)[R- ;ID0g9Q0joBd2(DHp-.&|K`nS¬l "(!r2 q@א!A3GM~,?: 0T`^nrKofܚ>`f5Ր-]h.O׬Ck!tƪC0bT 0}D P{yIޔ<,.<IL"[yKW[P&äC 'CyESSʱ*vIiR. 3y0n$c&> uyˢIw/ x'OdSolcdd* Rg^!+to?ps1JTj#; `t'0Li B2v2Pȋ~1e,pR﷥\S) (ҹ6~16ֹvkmv9 .hZzUy[g}yFQO;fe9rAͽq~\#,ާlBw'H癐ݢj6 ]I˜$uxF}1^PL)h)Y [꺔Or,m]]x&>yVi9ylu>.ǥ gc>YEӮD)I1XKH==敫Jk mv aq2zex# .`l-N~=$ Q./ZΎ}Z !eBX3wd4VK̡6yizGBy$;upappr!o)5i "Psy+%=>"tQX1 @,fsSoi b/o8/(6CztGNo/;W0d{_+2]o,ڝ~ 0E pUC옃[XJ[+gWMe!3 x7i1wP햚v61`HHCpm1\jl\.]Ss?y },aZC7ə}l 1 юzg5_ub- EFBug@^-Ȁ&$iSJ0P( 0Pn$ 9 p./P\Ѐ{h Jc[ߦjj ǁ"toBugl}}ǡjk ЏЅȟG;NΏ\wqWTT( dc`.fc lUy r(B9𺅰wr N r^c[c[8n еXWsl>n][;@ΕxE;R̥   &N~f(EU( aJIENDB`plumbum-1.8.3/docs/_static/placeholder0000644000000000000000000000000014613634536014716 0ustar00plumbum-1.8.3/docs/_templates/placeholder0000644000000000000000000000000014613634536015425 0ustar00plumbum-1.8.3/docs/api/cli.rst0000644000000000000000000000046714613634536013156 0ustar00.. _api-cli: Package plumbum.cli =================== .. automodule:: plumbum.cli.application :members: .. automodule:: plumbum.cli.switches :members: .. automodule:: plumbum.cli.terminal :members: .. automodule:: plumbum.cli.termsize :members: .. automodule:: plumbum.cli.progress :members: plumbum-1.8.3/docs/api/colors.rst0000644000000000000000000000112114613634536013674 0ustar00Package plumbum.colors ====================== .. automodule:: plumbum.colors :members: :special-members: plumbum.colorlib ---------------- .. automodule:: plumbum.colorlib :members: :special-members: plumbum.colorlib.styles ----------------------- .. automodule:: plumbum.colorlib.styles :members: :special-members: plumbum.colorlib.factories -------------------------- .. automodule:: plumbum.colorlib.factories :members: :special-members: plumbum.colorlib.names ---------------------- .. automodule:: plumbum.colorlib.names :members: :special-members: plumbum-1.8.3/docs/api/commands.rst0000644000000000000000000000056514613634536014207 0ustar00.. _api-commands: Package plumbum.commands ======================== .. automodule:: plumbum.commands.base :members: :special-members: .. automodule:: plumbum.commands.daemons :members: :special-members: .. automodule:: plumbum.commands.modifiers :members: :special-members: .. automodule:: plumbum.commands.processes :members: :special-members: plumbum-1.8.3/docs/api/fs.rst0000644000000000000000000000023414613634536013007 0ustar00Package plumbum.fs ================== File system utilities .. automodule:: plumbum.fs.atomic :members: .. automodule:: plumbum.fs.mounts :members: plumbum-1.8.3/docs/api/machines.rst0000644000000000000000000000112314613634536014164 0ustar00.. _api-local-machine: Package plumbum.machines ======================== .. automodule:: plumbum.machines.env :members: :special-members: .. automodule:: plumbum.machines.local :members: :special-members: .. automodule:: plumbum.machines.session :members: :special-members: .. _api-remote-machines: Remote Machines --------------- .. automodule:: plumbum.machines.remote :members: :special-members: .. automodule:: plumbum.machines.ssh_machine :members: :special-members: .. automodule:: plumbum.machines.paramiko_machine :members: :special-members: plumbum-1.8.3/docs/api/path.rst0000644000000000000000000000053414613634536013336 0ustar00.. _api-path: Package plumbum.path ==================== .. automodule:: plumbum.path.base :members: :special-members: .. automodule:: plumbum.path.local :members: :special-members: .. automodule:: plumbum.path.remote :members: :special-members: Utils ----- .. automodule:: plumbum.path.utils :members: :special-members: plumbum-1.8.3/examples/.gitignore0000644000000000000000000000011314613634536013746 0ustar00testfigure.pdf testfigure.svg testfigure.log testfigure.aux testfigure.png plumbum-1.8.3/examples/PHSP.png0000644000000000000000000003756414613634536013262 0ustar00PNG  IHDR=1-bKGDC IDATx$eyn͊5כuÙrٝ PBGUB%bANj49ZQEvo4GPpwzUVAAP#}=3μtLO(=}zzޙ3>O?a`=a5A@ @M&Pt :j5A@ @M&Pt :j5A@ @M&Pt :j5A@ @M&4 0Ioވ _~\&PtKcIa$IQK*ܹ( yzx8:oaaEfx d~7|) @EQ$gfؾZVەx$}½nԙfi$^n˷n0ehb`Y @I5!r2.c3Oȑ#=@3-=-8Iɭa8qDZZS͏`c13'VKq۝@ǝNlr 䲧ͷH8v% $aucN-~0k% i᫣ݏ3߫ $IڮnHV :EgmC:-w#?x{bs ę\Օqٔ.je$Fm>VL_@}DȖptr8# r]:'=/F?;Lض2y_I쉟 a{Waz8]n6*&Ch3EQ| +ם:r4Mͻδ,7x[2ϓ7w~~oG.>{n|mc'.rLzE|r} ̿$IBCf`Roǎma,P[ʘk>]H4Mvur*in+61 cZ-|&/,/b+s{t|ݎwwʕ^n+`}NK̝b4MGxWmoKTwuÇKgĩ(mͨ/vUa8iPlz`A?:H0 \dQPvuMoOO!rz{^? 8ɷm t)I8 EQ|t:@q@5$I~r&1]I+2O'x..LfL}T^O0 &k \r fji@yٓ-:Y$lI}خ/^:QP?KOjg4cN Lnkg&zYc瞻{nrev)ڑm١/l<<3 h#Z-jOffN<DEGvҌNi9rdVK~9oN|^gVhKmo%9t}j$2H1eƦ DoM0[ 7,Rilg}úm WxMՅKcccr_CwWto%闽^/Ff3rEuvywYV%;R#9-J`Jͼr1օvʽG3V2r.u)m,5bھ+>c.S.oГ$qhh .<Яꪷ>=ќѣaڵ몫*oBxW tMo}[o 7ڵ+xNaxO} 3m{k'O;<5KuzNe n{y=z4M/| G}ы^UW]uM7'Aql_ |Y}QOXڅ1&_n8e*mw!}"ss(j;M=0ۂD~PY繶 'w+;y˙]J(mA#v( */o)5]9$߽8cO㓮i\z뢹<'@2Gٽ{|Wue_!Eo&U~R.EMN3 !-eT$6*,=:h>̼m~8:|ߵHxGܐ `.` 8p@:޽{qF<PC_G;&(ZG^}wg&dA"YS^N0h~A$mI̵fX2#-4ur ozN^TطK![wbub'd]shdkT7{*5G&WGsϓ=Жme\R(.߉}X^>6&E)KLB2^`&vucWolb؀皷uwK=4Mvٔ3aΤ;%+ϩn nn%3o:\<}kg0 6tq".ZDy( Q^3NeaJe0lJ`,d$^4ؗÇ8 _>|8MS| vY0\/8vyJ>?sr2bߩK yjMe3[TwUn:{:N1]F\unv5/P'۵ iu$ DZYUߘkי; rsviJяמ+=z3Ÿ@+IWW,:|ȣf L InT}$״QnY+qcV,X$߲X_St&]Yd6%sdĄ͓F$й@ydQÖ ';$IF,p_*YUN%!\F|XYYY[[ۻw h4Ult—L}]\Ƿv_SŸ ^.⻌Dr8M}:tHߺ}]*N|eјl0Ckkk[ }]*^q.iŸ7dv)uًt7[F@L jIx0`HV+ArС-xۋʉӷگ6ʸeSڸ;iF+++KKK]`wW-..isWє KrѴگ#A YJv뙫ɑ[b;s-:]0o rD@P-z+'zڻk.|ͶFQ$XT6*fd$ڵf9%7W3רL*V a/=_'nUZrDOGo1O:>}y1\g&uvm!]K"XC׌5|Cx^S_.xΜ݁.[$>c:twsYE@h Nuhll1ײK[m.4K]r_.k2u|֋uuOe@]q5A@ @MTZ( ՛^&4?nsY-OEKk\eߚo*{:皸uydvD٢;`ChK㼏l.VрrPр'lpmM5C %{a B_߼kM ^2x-[uLJ2dJ6{LСNȡ^(,AFH}j:*XI[r>@e ֯J* 8r>V6A!2Y"D5KyHBT4`ÚڊEtq::Et--*{Brf[.;OptpJry.l%ǮMe?I𥞰vӑ_NjJ+ZtnW/ #Zb.YIBr&.ѹ ]c,AgYv(לm{U:M)-5|(E@PD@P~Mz"6 'r} Pf|nL'U%Z͗~y.B-m[]~]VWV#?'{R Z@!fi˭?)[| ?~79vW9n;KblzǷ72f)oJDžK< {w{Kt|ƧT']}*S:j5Q%tVݾCvO-iU}k2sm\R&^2 ضK_<Ojr j5A@h}x㧿/>wͮ0m}Efrae} ~i;$×uxjCOW`z04I$0 ۷/ 0 WVV*z(lXY Y1lz=h-,,AT3tؘ8"gi&I"\YYYY[[ۻwB>ޯC/8O׷kBe4u{Lƶ=rqib}L]$o_[nݯ=e}:~hia⹡$t:r&e lv:8WWW 8xlo4@UZfi;I ]5~`NINļ::ťBO~ǽ^|aaa W=|/~};uc=Ms]z՗v6-?/}Is&kckpߜR%l\PTo6.ǿ+x8Xv^~ـ(j۝NAn5////--H-@DQYqn7MSqqq1M4M~A,..ʀ3tv:~E)Գqֱ.Mrʶ}l/лeߒG粴C^6 yz'#^uuO7=vP*Їf2 Ҽ:DEЕYu/GǛ,w[.iYSvRҾ} LTm>&ssߚ<=eĥӕ'rtu7Mz)KןySX3t :j9t>OǗ_Ƿd;OKvL/.Z]2U4WEz&rt}KMcJmG:CǛd!u)/ji:l vE](?>u|E|ͥcȧ9t :jbrf3-_<ľr]%on՛j}[YBwkw 'cSn=ϹV\mOrW:aOp5A@ @MSt:>ˍd<]Z{}q5A@ה5_x]&.wlwik~텯7\*|VzL[rpC@moYp:޺:@M&*r:h@;_\sXjΗ7̺_m)Q[4e.ieqll$zN Aӷܮ:޺瀎ɧW) :j9ltߞ%%қo&osjYcn(m6n'n6ę{؏+%].O 5~>{yɧW gPt :ļMoQVfyskݺCܫ:إ٥5A].Oxߥ'Deƥ64?9OSg=u8C(%qaQ 'E)(z^ a8>~M53}ӯX] |\2c)vvJ;߿m,~CfrOԊV| Ie3N:~~]LM=I^n8 N$@N$nˉ9i[E |E$X$Inȥ;.TVbՒAm9It:~t-F]6&]wj|w\UQ7ncU{e./ķ"}y_4W5N2} i'RWfER휰seEr.@` }CVVVrt:cϯ0l6i92[fs`{X[[[XX`iiNGͣ(VK*eZ;G~hAV?_:M韐IDATOj߻3ϓS'22{z.%>-\ _W[G7nF|K aqݻA4S.Q^j0lK$fSiZ:lX5z.ͦ\\]] @yFc8"Ka#؀F|gI 誔>' ';7۴6sMX-9z.<*w.iyco0rdlԆm\.7{>Atͷiaaa >fotͷUF:̞ھ̱\#m4>t=- ͚ncZʡCF5+-G۶c:/2vʺ2^'\.yZ_Mw]|T\`pY Ѕuf9c`/c;u[I慄>#Oju֤|ܾ *!4MnMԼؾrur6E-[<' *7Jl.彨6.+nJA^TD\l^s6w6oʷ~ F j5A@siǷ^Rإ*3j6(5U]TMw=}{U_ ?W:~AP(jb\rt|^OQڎP7afJ>^+LcM; (mܚi .O9?D%0uƧ>P߸I֩EC_ oUр^-\z\+O&ijdG[ab.彚xb&^>bUgg^s@M&P]nxcI-M'YN1i{|Ut&6edQ.%O -1y. /&Pt r}xNo1y4nQ;liuN嘶p{Rsm۠?3u\F j5Aʥϼ'el]Skku%^M,f4jpc~&y*7t9-o6k-*Ї[PFh%2@80 ($IDQ$o>yJE`5 z^v[VfbIF=>y8K۷/aۭo-2\w]>7,>q򤾷15̓qpfԉNeowxE0Op>`mmm` =&>6^K+s)Od˛uYփLmcqA>svOWJou\`Mu>fODU.b:9tnw jfs:cFh4}VWW 0{. Z1C2l =@fAɫ\ڷw :Ri.716F3V$қʊgrI$I$Z*eVz0۲E\EDtcYqWyɥS[ wSMer}<'n8h{Qi k:x<#o]|՝M@{ ]Ōo,U CFL3i'ZC_-f% C-^khMwJrR]WFҔj!]/m=o.|5e-ϛI0~*<×>~iz҇nzL"̪\4(lKbfԤSy8[=L'uO?}vO{fZn(sS"OYSö}?@sg>y.Bq& %k%L<ؑ|_(*͸ʅr( &P,A7{o8['_~|ǦѾ9<=^|[>nMź,[DoǖV5)*Ŝmʷː\ m(swعUgPt :9b{t-㗩Syjm^˶Kme|w%'s,I$}K:N{:6&ߋDO9};7޹R8Cp?wf}·yO{m3.J˸[ݥE߷o-S]KEIm?-6Y;~eۮ3>lr @M&*Ck^lZRVFy(&os>벱>\Ry,m|Ng?''Ϛy.om[n;4_ܒˌq5A@ @MC/3-te׾qߜom>wuys*v%/bc7Xy~yT}J}/l\y >Y:j5A\f{~Ru_эfQR_oq\Im .|mmm|y̢轞4Yռ:j5AB~rVt.ֱd]~&2Z|6J)MJ=M}|Ӝʓϳm.رys_ETI{c}5Ƙwi:j2++f:*7Bbrj 0IsK$QaE 0 ÕÖ~ ~qIqc*EL/e@4KI  Y|[>ec;k8j h6^/ Ca4 8CJ8 N3]4MDB{.,,(jfs`{376M A\>!,.9v@L'U7PhC@zX6 sUR.sƖOzg/o{󴩨V.)~euc)}C|L92o*mypmj#,JmZqŃ:th:t&匝NG,///--i+R.P!Qi:Pqn7MSV4M4 Cf5z2 1[>a|SS^T#7;SDoҝ5Ooo.rT;f끫,mk~_n&Rȱ\v}YLwûenmUrǬ+}Ƈ,_Lq.zzN 1nDs( s&JCGJg!vSǾR:ܺ2Z˖ԜS\wܲ[ToagOo},+OE:j5QX̥^O^3ſ \-ra7gnywj{Eo%ޠco٪c _-s*`P-[nwNFbzs_:kt\F\3ɥIri31ʉ{5p_{{@P":D\6 O\:wLe|o/j<+"(}K;y3$^Q%.$Lm\Bj5Ql59tdz_q/xm7{ 0}o>8.;+oxg+!(j5QlЙy3O]s[gr;MO{9SsխmW00uDZ:\:²p.ȡJD@ @MT1[: =~s0F3S<_ǧ'EǛuiLkO~83˧7<_eU'{o8fe~s]b L_R.g3/v <|ykMOk'-|UPQ80 (&xx:@ǝNl6^7ALb7?Ц9Gݯ36 ޵;#KW:>~M&/מ7=ns~G?}79~NGWsҝ}˭:lRDCðl&I|`4˗2/${H7 U`4KӴn3fs>!t7򣲹_yY p.Ӛ.p5A#]9#G}O؆7d&4$Iqb<(j՟<{C 4),ӮOe'<ʬ̟۩_6@u?s_0k&yV q5Qɧ_#TƢs/'z^O>ʝN'I*^9?"itdK5/mIZ DA%Itv;efG 2!ǟk!Xq9 rɌOO$+R£2?$&mԄOX.6VeЪ(j=o7f=Ӽ\6O"I0 %7t h9L͓EI[mn|eXy'lC/9&+*Ә1GPT@M&ۈ8 1vB@0ZVeIJqk7V5Pe~e} w `:\dުrFHKov%{Ds!ϲdX`mQ ܻ/]qhyy9]fY:@M \F i Qdq5/rPv[LJwf@?Mv4^>tu`.([f#3j&[@M4M}Ԣ ؝&d-[5Wț*̞ :P,J&)qW@}Hη q7MMz= ;*{oslRB3rla\5 v:̘4UM1`] @MP5A@ @M&Pt :j5A@ @M#ײ聑nIENDB`plumbum-1.8.3/examples/SimpleColorCLI.py0000644000000000000000000000077014613634536015121 0ustar00from plumbum import cli, colors # from plumbum.colorlib import HTMLStyle, StyleFactory # plumbum.colors = StyleFactory(HTMLStyle) class MyApp(cli.Application): PROGNAME = colors.green VERSION = colors.blue | "1.0.2" COLOR_GROUPS = {"Meta-switches": colors.yellow} COLOR_GROUP_TITLES = {"Meta-switches": colors.bold & colors.yellow} opts = cli.Flag("--ops", help=colors.magenta | "This is help") def main(self): print("HI") if __name__ == "__main__": MyApp.run() plumbum-1.8.3/examples/alignment.py0000755000000000000000000000054614613634536014323 0ustar00#!/usr/bin/env python3 from plumbum import cli class App(cli.Application): # VERSION = "1.2.3" # x = cli.SwitchAttr("--lala") y = cli.Flag("-f") def main(self, x, y): pass @App.subcommand("bar") class Bar(cli.Application): z = cli.Flag("-z") def main(self, z, w): pass if __name__ == "__main__": App.run() plumbum-1.8.3/examples/color.py0000755000000000000000000000130014613634536013450 0ustar00#!/usr/bin/env python3 from plumbum import colors with colors.fg.red: print("This is in red") print("This is completely restored, even if an exception is thrown!") print("The library will restore color on exiting automatically.") print(colors.bold["This is bold and exciting!"]) print(colors.bg.cyan | "This is on a cyan background.") print(colors.fg[42] | "If your terminal supports 256 colors, this is colorful!") print() for c in colors: print(c + "\u2588", end="") colors.reset() print() print("Colors can be reset " + colors.underline["Too!"]) for c in colors[:16]: print(c["This is in color!"]) colors.red() print("This should clean up the color automatically on program exit...") plumbum-1.8.3/examples/filecopy.py0000755000000000000000000000205014613634536014147 0ustar00#!/usr/bin/env python3 import logging from plumbum import cli, local from plumbum.path.utils import copy, delete logger = logging.getLogger("FileCopier") class FileCopier(cli.Application): overwrite = cli.Flag("-o", help="If given, overwrite existing files") @cli.switch(["-l", "--log-to-file"], argtype=str) def log_to_file(self, filename): """logs all output to the given file""" handler = logging.FileHandler(filename) logger.addHandler(handler) @cli.switch(["--verbose"], requires=["--log-to-file"]) def set_debug(self): """Sets verbose mode""" logger.setLevel(logging.DEBUG) def main(self, src, dst): if local.path(dst).exists(): if not self.overwrite: logger.debug("Oh no! That's terrible") raise ValueError("Destination already exists") delete(dst) logger.debug("I'm going to copy %s to %s", src, dst) copy(src, dst) logger.debug("Great success") if __name__ == "__main__": FileCopier.run() plumbum-1.8.3/examples/fullcolor.py0000755000000000000000000000041614613634536014342 0ustar00#!/usr/bin/env python3 from plumbum import colors with colors: print("Do you believe in color, punk? DO YOU?") for i in range(0, 255, 10): for j in range(0, 255, 10): print("".join(colors.rgb(i, j, k)["\u2588"] for k in range(0, 255, 10))) plumbum-1.8.3/examples/geet.py0000755000000000000000000000644314613634536013273 0ustar00#!/usr/bin/env python3 """ Examples:: $ python3 geet.py no command given $ python3 geet.py leet unknown command 'leet' $ python3 geet.py --help geet v1.7.2 The l33t version control Usage: geet.py [SWITCHES] [SUBCOMMAND [SWITCHES]] args... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Subcommands: commit creates a new commit in the current branch; see 'geet commit --help' for more info push pushes the current local branch to the remote one; see 'geet push --help' for more info $ python3 geet.py commit --help geet commit v1.7.2 creates a new commit in the current branch Usage: geet commit [SWITCHES] Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Switches: -a automatically add changed files -m VALUE:str sets the commit message; required $ python3 geet.py commit -m "foo" committing... """ try: import colorama colorama.init() except ImportError: pass from plumbum import cli, colors class Geet(cli.Application): SUBCOMMAND_HELPMSG = False DESCRIPTION = colors.yellow | """The l33t version control""" PROGNAME = colors.green VERSION = colors.blue | "1.7.2" COLOR_USAGE_TITLE = colors.bold | colors.magenta COLOR_USAGE = colors.magenta _group_names = ["Meta-switches", "Switches", "Sub-commands"] COLOR_GROUPS = dict( zip(_group_names, [colors.do_nothing, colors.skyblue1, colors.yellow]) ) COLOR_GROUP_TITLES = dict( zip( _group_names, [colors.bold, colors.bold | colors.skyblue1, colors.bold | colors.yellow], ) ) verbosity = cli.SwitchAttr( "--verbosity", cli.Set("low", "high", "some-very-long-name", "to-test-wrap-around"), help=colors.cyan | "sets the verbosity level of the geet tool. doesn't really do anything except for testing line-wrapping " "in help " * 3, ) verbositie = cli.SwitchAttr( "--verbositie", cli.Set("low", "high", "some-very-long-name", "to-test-wrap-around"), help=colors.hotpink | "sets the verbosity level of the geet tool. doesn't really do anything except for testing line-wrapping " "in help " * 3, ) @Geet.subcommand(colors.red | "commit") class GeetCommit(cli.Application): """creates a new commit in the current branch""" auto_add = cli.Flag("-a", help="automatically add changed files") message = cli.SwitchAttr("-m", str, mandatory=True, help="sets the commit message") def main(self): print("committing...") GeetCommit.unbind_switches("-v", "--version") @Geet.subcommand("push") class GeetPush(cli.Application): """pushes the current local branch to the remote one""" tags = cli.Flag("--tags", help="whether to push tags (default is False)") def main(self, remote, branch="master"): print(f"pushing to {remote}/{branch}...") if __name__ == "__main__": Geet.run() plumbum-1.8.3/examples/make_figures.py0000755000000000000000000000140014613634536014774 0ustar00#!/usr/bin/env python3 from plumbum import FG, cli, local from plumbum.cmd import convert, pdflatex from plumbum.path.utils import delete def image_comp(item): pdflatex["-shell-escape", item] & FG print("Converting", item) convert[item.with_suffix(".svg"), item.with_suffix(".png")] & FG delete( item.with_suffix(".log"), item.with_suffix(".aux"), ) class MyApp(cli.Application): def main(self, *srcfiles): print("Tex files should start with:") print(r"\documentclass[tikz,convert={outfile=\jobname.svg}]{standalone}") items = map(cli.ExistingFile, srcfiles) if srcfiles else local.cwd // "*.tex" for item in items: image_comp(item) if __name__ == "__main__": MyApp.run() plumbum-1.8.3/examples/simple_cli.py0000755000000000000000000000306114613634536014460 0ustar00#!/usr/bin/env python3 """ $ python3 simple_cli.py --help simple_cli.py v1.0 Usage: simple_cli.py [SWITCHES] srcfiles... Meta-switches: -h, --help Prints this help message and quits --version Prints the program's version and quits Switches: -I VALUE:str Specify include directories; may be given multiple times --loglevel LEVEL:int Sets the log-level of the logger -v, --verbose Enable verbose mode $ python3 simple_cli.py x.cpp y.cpp z.cpp Verbose: False Include dirs: [] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') $ python3 simple_cli.py -v Verbose: True Include dirs: [] Compiling: () $ python3 simple_cli.py -v -Ifoo/bar -Ispam/eggs Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: () $ 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') """ 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() plumbum-1.8.3/examples/testfigure.tex0000644000000000000000000000051214613634536014664 0ustar00\documentclass[tikz,convert={outfile=\jobname.svg}]{standalone} %\usetikzlibrary{...}% tikz package already loaded by 'tikz' option \begin{document} \begin{tikzpicture}% Example: \draw (0,0) -- (10,10); % ... \draw (10,0) -- (0,10); % ... \node at (5,5) {Lorem ipsum at domine standalonus}; \end{tikzpicture} \end{document} plumbum-1.8.3/experiments/parallel.py0000644000000000000000000001300414613634536014654 0ustar00from plumbum.commands.base import BaseCommand from plumbum.commands.processes import CommandNotFound, ProcessExecutionError, run_proc 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 if isinstance(rhs, ConcurrentCommand): rhs.commands.insert(0, self) return rhs return ConcurrentCommand(self, rhs) BaseCommand.__and__ = make_concurrent class ConcurrentPopen: def __init__(self, procs): self.procs = procs self.stdin = None self.stdout = None self.stderr = None self.custom_encoding = None self.returncode = None @property def argv(self): return [getattr(proc, "argv", []) for proc in self.procs] def poll(self): if self.returncode is not None: return self.returncode rcs = [proc.poll() for proc in self.procs] if any(rc is None for rc in rcs): return None self.returncode = 0 for rc in rcs: if rc != 0: self.returncode = rc break return self.returncode def wait(self): for proc in self.procs: proc.wait() return self.poll() def communicate(self, input=None): if input: raise ValueError("Cannot pass input to ConcurrentPopen.communicate") out_err_tuples = [proc.communicate() for proc in self.procs] self.wait() return tuple(zip(*out_err_tuples)) class ConcurrentCommand(BaseCommand): def __init__(self, *commands): self.commands = list(commands) def formulate(self, level=0, args=()): form = ["("] for cmd in self.commands: form.extend(cmd.formulate(level, args)) form.append("&") return [*form, ")"] def popen(self, *args, **kwargs): return ConcurrentPopen([cmd[args].popen(**kwargs) for cmd in self.commands]) def __getitem__(self, args): """Creates a bound-command with the given arguments""" if not isinstance(args, (tuple, list)): args = [ args, ] if not args: return self return ConcurrentCommand(*(cmd[args] for cmd in self.commands)) class Cluster: def __init__(self, *machines): self.machines = list(machines) def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): for mach in self.machines: mach.close() del self.machines[:] def add_machine(self, machine): self.machines.append(machine) def __iter__(self): return iter(self.machines) def filter(self, pred): return self.__class__(filter(pred, self)) def which(self, progname): return [mach.which(progname) for mach in self] def list_processes(self): return [mach.list_processes() for mach in self] def pgrep(self, pattern): return [mach.pgrep(pattern) for mach in self] def path(self, *parts): return [mach.path(*parts) for mach in self] def __getitem__(self, progname): if not isinstance(progname, str): raise TypeError( "progname must be a string, not {!r}".format( type( progname, ) ) ) return ConcurrentCommand(*(mach[progname] for mach in self)) def __contains__(self, cmd): try: self[cmd] except CommandNotFound: return False else: return True @property def python(self): return ConcurrentCommand(*(mach.python for mach in self)) def session(self): return ClusterSession(*(mach.session() for mach in self)) class ClusterSession: def __init__(self, *sessions): self.sessions = sessions def __iter__(self): return iter(self.sessions) def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def __del__(self): try: # noqa: SIM105 self.close() except Exception: pass def alive(self): """Returns ``True`` if the underlying shells are all alive, ``False`` otherwise""" return all(session.alive for session in self) def close(self): """Closes (terminates) all underlying shell sessions""" for session in self.sessions: session.close() del self.sessions[:] def popen(self, cmd): return ConcurrentPopen([session.popen(cmd) for session in self]) def run(self, cmd, retcode=None): return run_proc(self.popen(cmd), retcode) if __name__ == "__main__": from plumbum import local from plumbum.cmd import date, ls, sleep c = ls & date & sleep[1] print(c()) c = ls & date & sleep[1] & sleep["-z"] try: c() except ProcessExecutionError as ex: print(ex) else: raise AssertionError("Expected an ProcessExecutionError") clst = Cluster(local, local, local) print(clst["ls"]()) # This works fine print(local.session().run("echo $$")) # this does not ret, stdout, stderr = clst.session().run("echo $$") print(ret) ret = [int(pid) for pid in stdout] assert len(set(ret)) == 3 plumbum-1.8.3/experiments/test_parallel.py0000644000000000000000000000235114613634536015716 0ustar00import unittest from parallel import Cluster from plumbum import SshMachine, local TEST_HOST = "127.0.0.1" class TestParallel(unittest.TestCase): def setUp(self): self.remotes = [] def connect(self): m = SshMachine(TEST_HOST) self.remotes.append(m) return m def tearDown(self): for m in self.remotes: m.close() def test_parallel(self): m = Cluster(local, local) import time t = time.time() ret = m["sleep"]("2") assert len(ret) == 2 assert 2 <= time.time() - t < 4 def test_locals(self): m = Cluster(local, local, local) # we should get 3 different proc ids ret = m["bash"]["-c"]["echo $$"]() ret = list(map(int, ret)) assert len(set(ret)) == 3 def test_sessions(self): m = Cluster(local, self.connect(), local, self.connect()) # we should get 4 different proc ids ret, stdout, stderr = m.session().run("echo $$") ret = [int(pid) for pid in stdout] assert len(set(ret)) == 4 def test_commands(self): cmds = local["echo"]["1"] & local["echo"]["2"] ret = cmds() a, b = map(int, ret) assert (a, b) == (1, 2) plumbum-1.8.3/plumbum/__init__.py0000644000000000000000000000630614613634536013744 0ustar00r""" Plumbum Shell Combinators ------------------------- A wrist-handy library for writing shell-like scripts in Python, that can serve as a ``Popen`` replacement, and much more:: >>> from plumbum.cmd import ls, grep, wc, cat >>> ls() 'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n' >>> chain = ls["-a"] | grep["-v", "py"] | wc["-l"] >>> print(chain) /bin/ls -a | /bin/grep -v py | /usr/bin/wc -l >>> chain() '12\n' >>> ((ls["-a"] | grep["-v", "py"]) > "/tmp/foo.txt")() '' >>> ((cat < "/tmp/foo.txt") | wc["-l"])() '12\n' >>> from plumbum import local, FG, BG >>> with local.cwd("/tmp"): ... (ls | wc["-l"]) & FG ... 13 # printed directly to the interpreter's stdout >>> (ls | wc["-l"]) & BG >>> f = _ >>> f.stdout # will wait for the process to terminate '9\n' Plumbum includes local/remote path abstraction, working directory and environment manipulation, process execution, remote process execution over SSH, tunneling, SCP-based upload/download, and a {arg|opt}parse replacement for the easy creation of command-line interface (CLI) programs. See https://plumbum.readthedocs.io for full details """ import sys # Avoids a circular import error later import plumbum.path # noqa: F401 from plumbum.commands import ( BG, ERROUT, FG, NOHUP, RETCODE, TEE, TF, CommandNotFound, ProcessExecutionError, ProcessLineTimedOut, ProcessTimedOut, ) from plumbum.machines import BaseRemoteMachine, PuttyMachine, SshMachine, local from plumbum.path import LocalPath, Path, RemotePath from plumbum.version import version __author__ = "Tomer Filiba (tomerfiliba@gmail.com)" __version__ = version __all__ = ( "BG", "ERROUT", "FG", "NOHUP", "RETCODE", "TEE", "TF", "CommandNotFound", "ProcessExecutionError", "ProcessLineTimedOut", "ProcessTimedOut", "BaseRemoteMachine", "PuttyMachine", "SshMachine", "local", "LocalPath", "Path", "RemotePath", "__author__", "__version__", "cmd", ) # =================================================================================================== # Module hack: ``from plumbum.cmd import ls`` # Can be replaced by a real module with __getattr__ after Python 3.6 is dropped # =================================================================================================== if sys.version_info < (3, 7): # noqa: UP036 from types import ModuleType from typing import List class LocalModule(ModuleType): """The module-hack that allows us to use ``from plumbum.cmd import some_program``""" __all__ = () # to make help() happy __package__ = __name__ def __getattr__(self, name): try: return local[name] except CommandNotFound: raise AttributeError(name) from None __path__: List[str] = [] __file__ = __file__ cmd = LocalModule(__name__ + ".cmd", LocalModule.__doc__) sys.modules[cmd.__name__] = cmd else: from . import cmd def __dir__(): "Support nice tab completion" return __all__ plumbum-1.8.3/plumbum/_testtools.py0000644000000000000000000000116314613634536014400 0ustar00import os import platform import sys import pytest skip_without_chown = pytest.mark.skipif( not hasattr(os, "chown"), reason="os.chown not supported" ) skip_without_tty = pytest.mark.skipif(not sys.stdin.isatty(), reason="Not a TTY") skip_on_windows = pytest.mark.skipif( sys.platform == "win32", reason="Windows not supported for this test (yet)" ) xfail_on_windows = pytest.mark.xfail( sys.platform == "win32", reason="Windows not supported for this test (yet)" ) xfail_on_pypy = pytest.mark.xfail( platform.python_implementation() == "PyPy", reason="PyPy is currently not working on this test!", ) plumbum-1.8.3/plumbum/cmd.py0000644000000000000000000000044414613634536012745 0ustar00import plumbum def __getattr__(name: str) -> plumbum.machines.LocalCommand: """The module-hack that allows us to use ``from plumbum.cmd import some_program``""" try: return plumbum.local[name] except plumbum.CommandNotFound: raise AttributeError(name) from None plumbum-1.8.3/plumbum/colors.py0000644000000000000000000000106614613634536013504 0ustar00""" This module imitates a real module, providing standard syntax like from `plumbum.colors` and from `plumbum.colors.bg` to work alongside all the standard syntax for colors. """ import atexit import sys from plumbum.colorlib import ansicolors, main _reset = ansicolors.reset.now if __name__ == "__main__": main() else: # Don't register an exit if this is called using -m! atexit.register(_reset) sys.modules[__name__ + ".fg"] = ansicolors.fg sys.modules[__name__ + ".bg"] = ansicolors.bg sys.modules[__name__] = ansicolors # type: ignore[assignment] plumbum-1.8.3/plumbum/lib.py0000644000000000000000000000376414613634536012760 0ustar00import inspect import os import sys from contextlib import contextmanager from io import StringIO IS_WIN32 = sys.platform == "win32" class ProcInfo: def __init__(self, pid, uid, stat, args): self.pid = pid self.uid = uid self.stat = stat self.args = args def __repr__(self): return f"ProcInfo({self.pid!r}, {self.uid!r}, {self.stat!r}, {self.args!r})" @contextmanager def captured_stdout(stdin=""): """ Captures stdout (similar to the redirect_stdout in Python 3.4+, but with slightly different arguments) """ prevstdin = sys.stdin prevstdout = sys.stdout sys.stdin = StringIO(stdin) sys.stdout = StringIO() try: yield sys.stdout finally: sys.stdin = prevstdin sys.stdout = prevstdout class StaticProperty: """This acts like a static property, allowing access via class or object. This is a non-data descriptor.""" def __init__(self, function): self._function = function self.__doc__ = function.__doc__ def __get__(self, obj, klass=None): return self._function() def getdoc(obj): """ This gets a docstring if available, and cleans it, but does not look up docs in inheritance tree (Pre Python 3.5 behavior of ``inspect.getdoc``). """ try: doc = obj.__doc__ except AttributeError: return None if not isinstance(doc, str): return None return inspect.cleandoc(doc) def read_fd_decode_safely(fd, size=4096): """ This reads a utf-8 file descriptor and returns a chunk, growing up to three bytes if needed to decode the character at the end. Returns the data and the decoded text. """ data = os.read(fd.fileno(), size) for _ in range(3): try: return data, data.decode("utf-8") except UnicodeDecodeError as e: if e.reason != "unexpected end of data": raise data += os.read(fd.fileno(), 1) return data, data.decode("utf-8") plumbum-1.8.3/plumbum/typed_env.py0000644000000000000000000001146114613634536014200 0ustar00import inspect import os from collections.abc import MutableMapping NO_DEFAULT = object() # must not inherit from AttributeError, so not to mess with python's attribute-lookup flow class EnvironmentVariableError(KeyError): pass class TypedEnv(MutableMapping): """ This object can be used in 'exploratory' mode: nv = TypedEnv() print(nv.HOME) It can also be used as a parser and validator of environment variables: class MyEnv(TypedEnv): username = TypedEnv.Str("USER", default='anonymous') path = TypedEnv.CSV("PATH", separator=":") tmp = TypedEnv.Str("TMP TEMP".split()) # support 'fallback' var-names nv = MyEnv() print(nv.username) for p in nv.path: print(p) try: print(p.tmp) except EnvironmentVariableError: print("TMP/TEMP is not defined") else: assert False """ __slots__ = ["_env", "_defined_keys"] class _BaseVar: def __init__(self, name, default=NO_DEFAULT): self.names = tuple(name) if isinstance(name, (tuple, list)) else (name,) self.name = self.names[0] self.default = default def convert(self, value): # pylint:disable=no-self-use return value def __get__(self, instance, owner): if not instance: return self try: return self.convert(instance._raw_get(*self.names)) except EnvironmentVariableError: if self.default is NO_DEFAULT: raise return self.default def __set__(self, instance, value): instance[self.name] = value class Str(_BaseVar): pass class Bool(_BaseVar): """ Converts 'yes|true|1|no|false|0' to the appropriate boolean value. Case-insensitive. Throws a ``ValueError`` for any other value. """ def convert(self, value): value = value.lower() if value not in {"yes", "no", "true", "false", "1", "0"}: raise ValueError(f"Unrecognized boolean value: {value!r}") return value in {"yes", "true", "1"} def __set__(self, instance, value): instance[self.name] = "yes" if value else "no" class Int(_BaseVar): convert = staticmethod(int) class Float(_BaseVar): convert = staticmethod(float) class CSV(_BaseVar): """ Comma-separated-strings get split using the ``separator`` (',' by default) into a list of objects of type ``type`` (``str`` by default). """ def __init__(self, name, default=NO_DEFAULT, type=str, separator=","): # pylint:disable=redefined-builtin super().__init__(name, default=default) self.type = type self.separator = separator def __set__(self, instance, value): instance[self.name] = self.separator.join(map(str, value)) def convert(self, value): return [self.type(v.strip()) for v in value.split(self.separator)] # ========= def __init__(self, env=None): if env is None: env = os.environ self._env = env self._defined_keys = { k for (k, v) in inspect.getmembers(self.__class__) if isinstance(v, self._BaseVar) } def __iter__(self): return iter(dir(self)) def __len__(self): return len(self._env) def __delitem__(self, name): del self._env[name] def __setitem__(self, name, value): self._env[name] = str(value) def _raw_get(self, *key_names): for key in key_names: value = self._env.get(key, NO_DEFAULT) if value is not NO_DEFAULT: return value raise EnvironmentVariableError(key_names[0]) def __contains__(self, key): try: self._raw_get(key) except EnvironmentVariableError: return False return True def __getattr__(self, name): # if we're here then there was no descriptor defined try: return self._raw_get(name) except EnvironmentVariableError: raise AttributeError( f"{self.__class__} has no attribute {name!r}" ) from None def __getitem__(self, key): return getattr(self, key) # delegate through the descriptors def get(self, key, default=None): try: return self[key] except EnvironmentVariableError: return default def __dir__(self): if self._defined_keys: # return only defined return sorted(self._defined_keys) # return whatever is in the environment (for convenience) members = set(self._env.keys()) members.update(dir(self.__class__)) return sorted(members) plumbum-1.8.3/plumbum/version.py0000644000000000000000000000063314613634536013667 0ustar00# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '1.8.3' __version_tuple__ = version_tuple = (1, 8, 3) plumbum-1.8.3/plumbum/cli/__init__.py0000644000000000000000000000113614613634536014507 0ustar00from .application import Application from .config import Config, ConfigINI from .switches import ( CSV, CountOf, ExistingDirectory, ExistingFile, Flag, NonexistentPath, Predicate, Range, Set, SwitchAttr, SwitchError, autoswitch, positional, switch, ) __all__ = ( "Application", "Config", "ConfigINI", "CSV", "CountOf", "ExistingDirectory", "ExistingFile", "Flag", "NonexistentPath", "Predicate", "Range", "Set", "SwitchAttr", "SwitchError", "autoswitch", "positional", "switch", ) plumbum-1.8.3/plumbum/cli/application.py0000644000000000000000000011224714613634536015261 0ustar00import functools import inspect import os import sys from collections import defaultdict from textwrap import TextWrapper from plumbum import colors, local from plumbum.cli.i18n import get_translation_for from plumbum.lib import getdoc from .switches import ( CountOf, Flag, MissingArgument, MissingMandatorySwitch, PositionalArgumentsError, SubcommandError, SwitchCombinationError, SwitchError, UnknownSwitch, WrongArgumentType, switch, ) from .terminal import get_terminal_size _translation = get_translation_for(__name__) T_, ngettext = _translation.gettext, _translation.ngettext class ShowHelp(SwitchError): pass class ShowHelpAll(SwitchError): pass class ShowVersion(SwitchError): pass class SwitchParseInfo: __slots__ = ["swname", "val", "index", "__weakref__"] def __init__(self, swname, val, index): self.swname = swname self.val = val self.index = index class Subcommand: def __init__(self, name, subapplication): self.name = name self.subapplication = subapplication def get(self): if isinstance(self.subapplication, str): modname, clsname = self.subapplication.rsplit(".", 1) mod = __import__(modname, None, None, "*") try: cls = getattr(mod, clsname) except AttributeError: raise ImportError(f"cannot import name {clsname}") from None self.subapplication = cls return self.subapplication def __repr__(self): return T_("Subcommand({self.name}, {self.subapplication})").format(self=self) _switch_groups = ["Switches", "Meta-switches"] _switch_groups_l10n = [T_("Switches"), T_("Meta-switches")] # =================================================================================================== # CLI Application base class # =================================================================================================== class Application: """The base class for CLI applications; your "entry point" class should derive from it, define the relevant switch functions and attributes, and the ``main()`` function. The class defines two overridable "meta switches" for version (``-v``, ``--version``) help (``-h``, ``--help``), and help-all (``--help-all``). The signature of the main function matters: any positional arguments (e.g., non-switch arguments) given on the command line are passed to the ``main()`` function; if you wish to allow unlimited number of positional arguments, use varargs (``*args``). The names of the arguments will be shown in the help message. The classmethod ``run`` serves as the entry point of the class. It parses the command-line arguments, invokes switch functions and enters ``main``. You should **not override** this method. Usage:: class FileCopier(Application): stat = Flag("p", "copy stat info as well") def main(self, src, dst): if self.stat: shutil.copy2(src, dst) else: shutil.copy(src, dst) if __name__ == "__main__": FileCopier.run() There are several class-level attributes you may set: * ``PROGNAME`` - the name of the program; if ``None`` (the default), it is set to the name of the executable (``argv[0]``); can be in color. If only a color, will be applied to the name. * ``VERSION`` - the program's version (defaults to ``1.0``, can be in color) * ``DESCRIPTION`` - a short description of your program (shown in help). If not set, the class' ``__doc__`` will be used. Can be in color. * ``DESCRIPTION_MORE`` - a detailed description of your program (shown in help). The text will be printed by paragraphs (specified by empty lines between them). The indentation of each paragraph will be the indentation of its first line. List items are identified by their first non-whitespace character being one of '-', '*', and '/'; so that they are not combined with preceding paragraphs. Bullet '/' is "invisible", meaning that the bullet itself will not be printed to the output. * ``USAGE`` - the usage line (shown in help). * ``COLOR_USAGE_TITLE`` - The color of the usage line's header. * ``COLOR_USAGE`` - The color of the usage line. * ``COLOR_GROUPS`` - A dictionary that sets colors for the groups, like Meta-switches, Switches, and Subcommands. * ``COLOR_GROUP_TITLES`` - A dictionary that sets colors for the group titles. If the dictionary is empty, it defaults to ``COLOR_GROUPS``. * ``SUBCOMMAND_HELPMSG`` - Controls the printing of extra "see subcommand -h" help message. Default is a message, set to False to remove. * ``ALLOW_ABBREV`` - Controls whether partial switch names are supported, for example '--ver' will match '--verbose'. Default is False for backward consistency with previous plumbum releases. Note that ambiguous abbreviations will not match, for example if --foothis and --foothat are defined, then --foo will not match. A note on sub-commands: when an application is the root, its ``parent`` attribute is set to ``None``. When it is used as a nested-command, ``parent`` will point to its direct ancestor. Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute will hold the chosen sub-application and its command-line arguments (a tuple); otherwise, it will be set to ``None`` """ PROGNAME = None DESCRIPTION = None DESCRIPTION_MORE = None VERSION = None USAGE = None COLOR_USAGE = None COLOR_USAGE_TITLE = None COLOR_GROUPS = None COLOR_GROUP_TITLES = None CALL_MAIN_IF_NESTED_COMMAND = True SUBCOMMAND_HELPMSG = T_("see '{parent} {sub} --help' for more info") ALLOW_ABBREV = False parent = None nested_command = None _unbound_switches = () def __new__(cls, executable=None): """Allows running the class directly as a shortcut for main. This is necessary for some setup scripts that want a single function, instead of an expression with a dot in it.""" if executable is None: # This return value was not a class instance, so __init__ is never called return cls.run() return super().__new__(cls) def __init__(self, executable): # Filter colors if self.PROGNAME is None: self.PROGNAME = os.path.basename(executable) elif isinstance(self.PROGNAME, colors._style): self.PROGNAME = self.PROGNAME | os.path.basename(executable) elif colors.filter(self.PROGNAME) == "": self.PROGNAME = colors.extract(self.PROGNAME) | os.path.basename(executable) if self.DESCRIPTION is None: self.DESCRIPTION = getdoc(self) # Allow None for the colors self.COLOR_GROUPS = defaultdict( lambda: colors.do_nothing, {} if type(self).COLOR_GROUPS is None else type(self).COLOR_GROUPS, ) self.COLOR_GROUP_TITLES = defaultdict( lambda: colors.do_nothing, self.COLOR_GROUPS if type(self).COLOR_GROUP_TITLES is None else type(self).COLOR_GROUP_TITLES, ) if type(self).COLOR_USAGE is None: self.COLOR_USAGE = colors.do_nothing self.executable = executable self._switches_by_name = {} self._switches_by_func = {} self._switches_by_envar = {} self._subcommands = {} for cls in reversed(type(self).mro()): for obj in cls.__dict__.values(): if isinstance(obj, Subcommand): name = colors.filter(obj.name) if name.startswith("-"): raise SubcommandError( T_("Sub-command names cannot start with '-'") ) # it's okay for child classes to override sub-commands set by their parents self._subcommands[name] = obj continue swinfo = getattr(obj, "_switch_info", None) if not swinfo: continue for name in swinfo.names: if name in self._unbound_switches: continue if ( name in self._switches_by_name and not self._switches_by_name[name].overridable ): raise SwitchError( T_( "Switch {name} already defined and is not overridable" ).format(name=name) ) self._switches_by_name[name] = swinfo self._switches_by_func[swinfo.func] = swinfo if swinfo.envname: self._switches_by_envar[swinfo.envname] = swinfo @property def root_app(self): return self.parent.root_app if self.parent else self @classmethod def unbind_switches(cls, *switch_names): """Unbinds the given switch names from this application. For example :: class MyApp(cli.Application): pass MyApp.unbind_switches("--version") """ cls._unbound_switches += tuple( name.lstrip("-") for name in switch_names if name ) @classmethod def subcommand(cls, name, subapp=None): """Registers the given sub-application as a sub-command of this one. This method can be used both as a decorator and as a normal ``classmethod``:: @MyApp.subcommand("foo") class FooApp(cli.Application): pass Or :: MyApp.subcommand("foo", FooApp) .. versionadded:: 1.1 .. versionadded:: 1.3 The sub-command can also be a string, in which case it is treated as a fully-qualified class name and is imported on demand. For example, MyApp.subcommand("foo", "fully.qualified.package.FooApp") """ def wrapper(subapp): subname = subapp if isinstance(subapp, str) else subapp.__name__ attrname = f"_subcommand_{subname}" setattr(cls, attrname, Subcommand(name, subapp)) return subapp return wrapper(subapp) if subapp else wrapper def _get_partial_matches(self, partialname): matches = [] for switch_ in self._switches_by_name: if switch_.startswith(partialname): matches += [ switch_, ] return matches def _parse_args(self, argv): tailargs = [] swfuncs = {} index = 0 while argv: index += 1 a = argv.pop(0) val = None if a == "--": # end of options, treat the rest as tailargs tailargs.extend(argv) break if a in self._subcommands: subcmd = self._subcommands[a].get() self.nested_command = ( subcmd, [self.PROGNAME + " " + self._subcommands[a].name, *argv], ) break if a.startswith("--") and len(a) >= 3: # [--name], [--name=XXX], [--name, XXX], [--name, ==, XXX], # [--name=, XXX], [--name, =XXX] eqsign = a.find("=") if eqsign >= 0: name = a[2:eqsign] argv.insert(0, a[eqsign:]) else: name = a[2:] if self.ALLOW_ABBREV: partials = self._get_partial_matches(name) if len(partials) == 1: name = partials[0] elif len(partials) > 1: raise UnknownSwitch( T_("Ambiguous partial switch {0}").format("--" + name) ) swname = "--" + name if name not in self._switches_by_name: raise UnknownSwitch(T_("Unknown switch {0}").format(swname)) swinfo = self._switches_by_name[name] if swinfo.argtype: if not argv: raise MissingArgument( T_("Switch {0} requires an argument").format(swname) ) a = argv.pop(0) if a and a[0] == "=": if len(a) >= 2: val = a[1:] else: if not argv: raise MissingArgument( T_("Switch {0} requires an argument").format(swname) ) val = argv.pop(0) else: val = a elif a.startswith("-") and len(a) >= 2: # [-a], [-a, XXX], [-aXXX], [-abc] name = a[1] swname = "-" + name if name not in self._switches_by_name: raise UnknownSwitch(T_("Unknown switch {0}").format(swname)) swinfo = self._switches_by_name[name] if swinfo.argtype: if len(a) >= 3: val = a[2:] else: if not argv: raise MissingArgument( T_("Switch {0} requires an argument").format(swname) ) val = argv.pop(0) elif len(a) >= 3: argv.insert(0, "-" + a[2:]) else: if a.startswith("-"): raise UnknownSwitch(T_("Unknown switch {0}").format(a)) tailargs.append(a) continue # handle argument val = self._handle_argument(val, swinfo.argtype, name) if swinfo.func in swfuncs: if swinfo.list: swfuncs[swinfo.func].val[0].append(val) else: if swfuncs[swinfo.func].swname == swname: raise SwitchError(T_("Switch {0} already given").format(swname)) raise SwitchError( T_("Switch {0} already given ({1} is equivalent)").format( swfuncs[swinfo.func].swname, swname ) ) else: if swinfo.list: swfuncs[swinfo.func] = SwitchParseInfo(swname, ([val],), index) elif val is NotImplemented: swfuncs[swinfo.func] = SwitchParseInfo(swname, (), index) else: swfuncs[swinfo.func] = SwitchParseInfo(swname, (val,), index) # Extracting arguments from environment variables envindex = 0 for env, swinfo in self._switches_by_envar.items(): envindex -= 1 envval = local.env.get(env) if envval is None: continue if swinfo.func in swfuncs: continue # skip if overridden by command line arguments val = self._handle_argument(envval, swinfo.argtype, env) envname = f"${env}" if swinfo.list: # multiple values over environment variables are not supported, # this will require some sort of escaping and separator convention swfuncs[swinfo.func] = SwitchParseInfo(envname, ([val],), envindex) elif val is NotImplemented: swfuncs[swinfo.func] = SwitchParseInfo(envname, (), envindex) else: swfuncs[swinfo.func] = SwitchParseInfo(envname, (val,), envindex) return swfuncs, tailargs @classmethod def autocomplete(cls, argv): """This is supplied to make subclassing and testing argument completion methods easier""" @staticmethod def _handle_argument(val, argtype, name): if argtype: try: return argtype(val) except (TypeError, ValueError) as ex: raise WrongArgumentType( T_( "Argument of {name} expected to be {argtype}, not {val!r}:\n {ex!r}" ).format(name=name, argtype=argtype, val=val, ex=ex) ) from None else: return NotImplemented def _validate_args(self, swfuncs, tailargs): if self.help.__func__ in swfuncs: raise ShowHelp() if self.helpall.__func__ in swfuncs: raise ShowHelpAll() if self.version.__func__ in swfuncs: raise ShowVersion() requirements = {} exclusions = {} for swinfo in self._switches_by_func.values(): if swinfo.mandatory and swinfo.func not in swfuncs: raise MissingMandatorySwitch( T_("Switch {0} is mandatory").format( "/".join( ("-" if len(n) == 1 else "--") + n for n in swinfo.names ) ) ) requirements[swinfo.func] = { self._switches_by_name[req] for req in swinfo.requires } exclusions[swinfo.func] = { self._switches_by_name[exc] for exc in swinfo.excludes } # TODO: compute topological order gotten = set(swfuncs.keys()) for func in gotten: missing = {f.func for f in requirements[func]} - gotten if missing: raise SwitchCombinationError( T_("Given {0}, the following are missing {1}").format( swfuncs[func].swname, [self._switches_by_func[f].names[0] for f in missing], ) ) invalid = {f.func for f in exclusions[func]} & gotten if invalid: raise SwitchCombinationError( T_("Given {0}, the following are invalid {1}").format( swfuncs[func].swname, [swfuncs[f].swname for f in invalid] ) ) m = inspect.getfullargspec(self.main) if sys.version_info < (3, 10): sig = inspect.signature(self.main) else: sig = inspect.signature(self.main, eval_str=True) max_args = sys.maxsize if m.varargs else len(m.args) - 1 min_args = len(m.args) - 1 - (len(m.defaults) if m.defaults else 0) if len(tailargs) < min_args: raise PositionalArgumentsError( ngettext( "Expected at least {0} positional argument, got {1}", "Expected at least {0} positional arguments, got {1}", min_args, ).format(min_args, tailargs) ) if len(tailargs) > max_args: raise PositionalArgumentsError( ngettext( "Expected at most {0} positional argument, got {1}", "Expected at most {0} positional arguments, got {1}", max_args, ).format(max_args, tailargs) ) # Positional argument validation if hasattr(self.main, "positional"): tailargs = self._positional_validate( tailargs, self.main.positional, self.main.positional_varargs, m.args[1:], m.varargs, ) elif hasattr(m, "annotations") and m.annotations: args_names = list(m.args[1:]) positional = [None] * len(args_names) varargs = None # All args are positional, so convert kargs to positional for item in m.annotations: annotation = ( sig.parameters[item].annotation if item != "return" else sig.return_annotation ) if sys.version_info < (3, 10) and isinstance(annotation, str): annotation = eval(annotation) if item == m.varargs: varargs = annotation elif item != "return": positional[args_names.index(item)] = annotation tailargs = self._positional_validate( tailargs, positional, varargs, m.args[1:], m.varargs ) ordered = [ (f, a) for _, f, a in sorted((sf.index, f, sf.val) for f, sf in swfuncs.items()) ] return ordered, tailargs def _positional_validate(self, args, validator_list, varargs, argnames, varargname): """Makes sure args follows the validation given input""" 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] ) if len(args) > len(validator_list): if varargs is not None: out_args[len(validator_list) :] = [ self._handle_argument(a, varargs, varargname) for a in args[len(validator_list) :] ] else: out_args[len(validator_list) :] = args[len(validator_list) :] return out_args @classmethod def run( cls, argv=None, exit=True, # pylint: disable=redefined-builtin ): """ Runs the application, taking the arguments from ``sys.argv`` by default if nothing is passed. If ``exit`` is ``True`` (the default), the function will exit with the appropriate return code; otherwise it will return a tuple of ``(inst, retcode)``, where ``inst`` is the application instance created internally by this function and ``retcode`` is the exit code of the application. .. note:: Setting ``exit`` to ``False`` is intendend for testing/debugging purposes only -- do not override it in other situations. """ if argv is None: argv = sys.argv cls.autocomplete(argv) argv = list(argv) inst = cls(argv.pop(0)) retcode = 0 try: swfuncs, tailargs = inst._parse_args(argv) ordered, tailargs = inst._validate_args(swfuncs, tailargs) except ShowHelp: inst.help() except ShowHelpAll: inst.helpall() except ShowVersion: inst.version() except SwitchError as ex: print(T_("Error: {0}").format(ex)) print(T_("------")) inst.help() retcode = 2 else: for f, a in ordered: f(inst, *a) cleanup = None if not inst.nested_command or inst.CALL_MAIN_IF_NESTED_COMMAND: retcode = inst.main(*tailargs) cleanup = functools.partial(inst.cleanup, retcode) if not retcode and inst.nested_command: subapp, argv = inst.nested_command subapp.parent = inst inst, retcode = subapp.run(argv, exit=False) if cleanup: cleanup() if retcode is None: retcode = 0 if exit: sys.exit(retcode) else: return inst, retcode @classmethod def invoke(cls, *args, **switches): """Invoke this application programmatically (as a function), in the same way ``run()`` would. There are two key differences: the return value of ``main()`` is not converted to an integer (returned as-is), and exceptions are not swallowed either. :param args: any positional arguments for ``main()`` :param switches: command-line switches are passed as keyword arguments, e.g., ``foo=5`` for ``--foo=5`` """ inst = cls("") swfuncs = inst._parse_kwd_args(switches) ordered, tailargs = inst._validate_args(swfuncs, args) for f, a in ordered: f(inst, *a) cleanup = None if not inst.nested_command or inst.CALL_MAIN_IF_NESTED_COMMAND: retcode = inst.main(*tailargs) cleanup = functools.partial(inst.cleanup, retcode) if not retcode and inst.nested_command: subapp, argv = inst.nested_command subapp.parent = inst inst, retcode = subapp.run(argv, exit=False) if cleanup: cleanup() return inst, retcode def _parse_kwd_args(self, switches): """Parses keywords (positional arguments), used by invoke.""" swfuncs = {} for index, (swname, val) in enumerate(switches.items(), 1): switch_local = getattr(type(self), swname) swinfo = self._switches_by_func[switch_local._switch_info.func] if isinstance(switch_local, CountOf): p = (range(val),) elif swinfo.list and not hasattr(val, "__iter__"): raise SwitchError( T_("Switch {0} must be a sequence (iterable)").format(swname) ) elif not swinfo.argtype: # a flag if val not in (True, False, None, Flag): raise SwitchError(T_("Switch {0} is a boolean flag").format(swname)) p = () else: p = (val,) swfuncs[swinfo.func] = SwitchParseInfo(swname, p, index) return swfuncs def main(self, *args): """Implement me (no need to call super)""" if self._subcommands: if args: print(T_("Unknown sub-command '{0}'").format(args[0])) print(T_("------")) self.help() return 1 if not self.nested_command: print(T_("No sub-command given")) print(T_("------")) self.help() return 1 return 0 print(T_("main() not implemented")) return 1 def cleanup(self, retcode): """Called after ``main()`` and all sub-applications have executed, to perform any necessary cleanup. :param retcode: the return code of ``main()`` """ @switch( ["--help-all"], overridable=True, group="Meta-switches", help=T_("""Prints help messages of all sub-commands and quits"""), ) def helpall(self): """Prints help messages of all sub-commands and quits""" self.help() print() if self._subcommands: for name, subcls in sorted(self._subcommands.items()): subapp = (subcls.get())(f"{self.PROGNAME} {name}") subapp.parent = self for si in subapp._switches_by_func.values(): if si.group == "Meta-switches": si.group = "Hidden-switches" subapp.helpall() @switch( ["-h", "--help"], overridable=True, group="Meta-switches", help=T_("""Prints this help message and quits"""), ) def help(self): # @ReservedAssignment """Prints this help message and quits""" if self._get_prog_version(): self.version() print() if self.DESCRIPTION: print(self.DESCRIPTION.strip() + "\n") def split_indentation(s): """Identifies the initial indentation (all spaces) of the string and returns the indentation as well as the remainder of the line. """ i = 0 while i < len(s) and s[i] == " ": i += 1 return s[:i], s[i:] def paragraphs(text): """Yields each paragraph of text along with its initial and subsequent indentations to be used by textwrap.TextWrapper. Identifies list items from their first non-space character being one of bullets '-', '*', and '/'. However, bullet '/' is invisible and is removed from the list item. :param text: The text to separate into paragraphs """ paragraph = None initial_indent = "" subsequent_indent = "" def current(): """Yields the current result if present.""" if paragraph: yield paragraph, initial_indent, subsequent_indent for part in text.lstrip("\n").split("\n"): indent, line = split_indentation(part) if len(line) == 0: # Starting a new paragraph yield from current() yield "", "", "" paragraph = None initial_indent = "" subsequent_indent = "" else: # Adding to current paragraph def is_list_item(line): """Returns true if the first element of 'line' is a bullet character.""" bullets = ["-", "*", "/"] return line[0] in bullets def has_invisible_bullet(line): """Returns true if the first element of 'line' is the invisible bullet ('/').""" return line[0] == "/" if is_list_item(line): # Done with current paragraph yield from current() if has_invisible_bullet(line): line = line[1:] paragraph = line initial_indent = indent # Calculate extra indentation for subsequent lines of this list item i = 1 while i < len(line) and line[i] == " ": i += 1 subsequent_indent = indent + " " * i else: if not paragraph: # Start a new paragraph paragraph = line initial_indent = indent subsequent_indent = indent else: # Add to current paragraph paragraph = paragraph + " " + line yield from current() def wrapped_paragraphs(text, width): """Yields each line of each paragraph of text after wrapping them on 'width' number of columns. :param text: The text to yield wrapped lines of :param width: The width of the wrapped output """ if not text: return width = max(width, 1) for paragraph, initial_indent, subsequent_indent in paragraphs(text): wrapper = TextWrapper( width, initial_indent=initial_indent, subsequent_indent=subsequent_indent, ) w = wrapper.wrap(paragraph) yield from w if len(w) == 0: yield "" cols, _ = get_terminal_size() for line in wrapped_paragraphs(self.DESCRIPTION_MORE, cols): print(line) m = inspect.getfullargspec(self.main) tailargs = m.args[1:] # skip self if m.defaults: for i, d in enumerate(reversed(m.defaults)): tailargs[-i - 1] = f"[{tailargs[-i - 1]}={d}]" if m.varargs: tailargs.append(f"{m.varargs}...") tailargs = " ".join(tailargs) utc = self.COLOR_USAGE_TITLE or self.COLOR_USAGE print(utc | T_("Usage:")) with self.COLOR_USAGE: if not self.USAGE: if self._subcommands: self.USAGE = T_( " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" ) else: self.USAGE = T_(" {progname} [SWITCHES] {tailargs}\n") print( self.USAGE.format( progname=colors.filter(self.PROGNAME), tailargs=tailargs ) ) by_groups = {} for si in self._switches_by_func.values(): if si.group not in by_groups: by_groups[si.group] = [] by_groups[si.group].append(si) def switchs(by_groups, show_groups): for grp, swinfos in sorted(by_groups.items(), key=lambda item: item[0]): if show_groups: lgrp = T_(grp) if grp in _switch_groups else grp print(self.COLOR_GROUP_TITLES[grp] | lgrp + ":") for si in sorted(swinfos, key=lambda x: x.names): swnames = ", ".join( ("-" if len(n) == 1 else "--") + n for n in si.names if n in self._switches_by_name and self._switches_by_name[n] == si ) if si.argtype: if hasattr(si.argtype, "__name__"): typename = si.argtype.__name__ else: typename = str(si.argtype) argtype = f" {si.argname.upper()}:{typename}" else: argtype = "" prefix = swnames + argtype yield si, prefix, self.COLOR_GROUPS[grp] if show_groups: print() sw_width = ( max(len(prefix) for si, prefix, color in switchs(by_groups, False)) + 4 ) description_indent = " {0}{1}{2}" wrapper = TextWrapper(width=max(cols - min(sw_width, 60), 50) - 6) indentation = "\n" + " " * (cols - wrapper.width) for switch_info, prefix, color in switchs(by_groups, True): help_txt = switch_info.help if switch_info.list: help_txt += T_("; may be given multiple times") if switch_info.mandatory: help_txt += T_("; required") if switch_info.requires: help_txt += T_("; requires {0}").format( ", ".join( (("-" if len(switch) == 1 else "--") + switch) for switch in switch_info.requires ) ) if switch_info.excludes: help_txt += T_("; excludes {0}").format( ", ".join( (("-" if len(switch) == 1 else "--") + switch) for switch in switch_info.excludes ) ) msg = indentation.join( wrapper.wrap(" ".join(ln.strip() for ln in help_txt.splitlines())) ) if len(prefix) + wrapper.width >= cols: padding = indentation else: padding = " " * max(cols - wrapper.width - len(prefix) - 4, 1) print(description_indent.format(color | prefix, padding, color | msg)) if self._subcommands: gc = self.COLOR_GROUP_TITLES["Sub-commands"] print(gc | T_("Sub-commands:")) for name, subcls in sorted(self._subcommands.items()): with gc: subapp = subcls.get() doc = subapp.DESCRIPTION or getdoc(subapp) if self.SUBCOMMAND_HELPMSG: help_str = doc + "; " if doc else "" help_str += self.SUBCOMMAND_HELPMSG.format( parent=self.PROGNAME, sub=name ) else: help_str = doc or "" msg = indentation.join( wrapper.wrap( " ".join(ln.strip() for ln in help_str.splitlines()) ) ) if len(name) + wrapper.width >= cols: padding = indentation else: padding = " " * max(cols - wrapper.width - len(name) - 4, 1) if colors.contains_colors(subcls.name): bodycolor = colors.extract(subcls.name) else: bodycolor = gc print( description_indent.format( subcls.name, padding, bodycolor | colors.filter(msg) ) ) def _get_prog_version(self): ver = None curr = self while curr is not None: ver = getattr(curr, "VERSION", None) if ver is not None: return ver curr = curr.parent return ver @switch( ["-v", "--version"], overridable=True, group="Meta-switches", help=T_("""Prints the program's version and quits"""), ) def version(self): """Prints the program's version and quits""" ver = self._get_prog_version() ver_name = ver if ver is not None else T_("(version not set)") print(f"{self.PROGNAME} {ver_name}") plumbum-1.8.3/plumbum/cli/config.py0000644000000000000000000000620014613634536014212 0ustar00import contextlib from abc import ABC, abstractmethod from configparser import ConfigParser, NoOptionError, NoSectionError from plumbum import local class ConfigBase(ABC): """Base class for Config parsers. :param filename: The file to use The ``with`` statement can be used to automatically try to read on entering and write if changed on exiting. Otherwise, use ``.read`` and ``.write`` as needed. Set and get the options using ``[]`` syntax. Usage: with Config("~/.myprog_rc") as conf: value = conf.get("option", "default") value2 = conf["option"] # shortcut for default=None """ __slots__ = "filename changed".split() def __init__(self, filename): self.filename = local.path(filename) self.changed = False def __enter__(self): with contextlib.suppress(FileNotFoundError): self.read() return self def __exit__(self, exc_type, exc_val, exc_tb): if self.changed: self.write() @abstractmethod def read(self): """Read in the linked file""" @abstractmethod def write(self): """Write out the linked file""" self.changed = False @abstractmethod def _get(self, option): """Internal get function for subclasses""" @abstractmethod def _set(self, option, value): """Internal set function for subclasses. Must return the value that was set.""" def get(self, option, default=None): "Get an item from the store, returns default if fails" try: return self._get(option) except KeyError: self.changed = True return self._set(option, default) def set(self, option, value): """Set an item, mark this object as changed""" self.changed = True self._set(option, value) def __getitem__(self, option): return self._get(option) def __setitem__(self, option, value): return self.set(option, value) class ConfigINI(ConfigBase): DEFAULT_SECTION = "DEFAULT" slots = "parser".split() def __init__(self, filename): super().__init__(filename) self.parser = ConfigParser() def read(self): self.parser.read(self.filename) super().read() def write(self): with open(self.filename, "w", encoding="utf-8") as f: self.parser.write(f) super().write() @classmethod def _sec_opt(cls, option): if "." not in option: sec = cls.DEFAULT_SECTION else: sec, option = option.split(".", 1) return sec, option def _get(self, option): sec, option = self._sec_opt(option) try: return self.parser.get(sec, option) except (NoSectionError, NoOptionError): raise KeyError(f"{sec}:{option}") from None def _set(self, option, value): sec, option = self._sec_opt(option) try: self.parser.set(sec, option, str(value)) except NoSectionError: self.parser.add_section(sec) self.parser.set(sec, option, str(value)) return str(value) Config = ConfigINI plumbum-1.8.3/plumbum/cli/i18n.py0000644000000000000000000000317514613634536013534 0ustar00import locale # High performance method for English (no translation needed) loc = locale.getlocale()[0] if loc is None or loc.startswith("en") or loc == "C": class NullTranslation: def gettext(self, str1: str) -> str: # pylint: disable=no-self-use return str1 def ngettext(self, str1, strN, n): # pylint: disable=no-self-use if n == 1: return str1.replace("{0}", str(n)) return strN.replace("{0}", str(n)) def get_translation_for( package_name: str, # noqa: ARG001 ) -> NullTranslation: return NullTranslation() else: import gettext import os # If not installed with setuptools, this might not be available try: import pkg_resources except ImportError: pkg_resources = None # type: ignore[assignment] local_dir = os.path.basename(__file__) def get_translation_for(package_name: str) -> gettext.NullTranslations: # type: ignore[misc] """Find and return gettext translation for package (Try to find folder manually if setuptools does not exist) """ if "." in package_name: package_name = ".".join(package_name.split(".")[:-1]) localedir = None if pkg_resources is None: mydir = os.path.join(local_dir, "i18n") else: mydir = pkg_resources.resource_filename(package_name, "i18n") for localedir in mydir, None: localefile = gettext.find(package_name, localedir) if localefile: break return gettext.translation(package_name, localedir=localedir, fallback=True) plumbum-1.8.3/plumbum/cli/image.py0000644000000000000000000000642314613634536014036 0ustar00import sys from plumbum import colors from .. import cli from .termsize import get_terminal_size class Image: __slots__ = "size char_ratio".split() def __init__(self, size=None, char_ratio=2.45): self.size = size self.char_ratio = char_ratio def best_aspect(self, orig, term): """Select a best possible size matching the original aspect ratio. Size is width, height. The char_ratio option gives the height of each char with respect to its width, zero for no effect.""" if not self.char_ratio: # Don't use if char ratio is 0 return term orig_ratio = orig[0] / orig[1] / self.char_ratio if int(term[1] / orig_ratio) <= term[0]: 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.""" import PIL.Image return ( self.show_pil_double(PIL.Image.open(filename)) if double else self.show_pil(PIL.Image.open(filename)) ) def _init_size(self, im): """Return the expected image size""" if self.size is None: term_size = get_terminal_size() return self.best_aspect(im.size, term_size) return self.size def show_pil(self, im): "Standard show routine" size = self._init_size(im) new_im = im.resize(size).convert("RGB") for y in range(size[1]): for x in range(size[0] - 1): pix = new_im.getpixel((x, y)) sys.stdout.write(colors.bg.rgb(*pix) + " ") # '\u2588' sys.stdout.write(colors.reset + " \n") sys.stdout.write(colors.reset + "\n") sys.stdout.flush() def show_pil_double(self, im): "Show double resolution on some fonts" size = self._init_size(im) size = (size[0], size[1] * 2) new_im = im.resize(size).convert("RGB") for y in range(size[1] // 2): for x in range(size[0] - 1): pix = new_im.getpixel((x, y * 2)) pixl = new_im.getpixel((x, y * 2 + 1)) sys.stdout.write( (colors.bg.rgb(*pixl) & colors.fg.rgb(*pix)) + "\u2580" ) sys.stdout.write(colors.reset + " \n") sys.stdout.write(colors.reset + "\n") sys.stdout.flush() class ShowImageApp(cli.Application): "Display an image on the terminal" double = cli.Flag( ["-d", "--double"], help="Double resolution (looks good only with some fonts)" ) @cli.switch(["-c", "--colors"], cli.Range(1, 4), help="Level of color, 1-4") def colors_set(self, n): # pylint: disable=no-self-use colors.use_color = n size = cli.SwitchAttr(["-s", "--size"], help="Size, should be in the form 100x150") ratio = cli.SwitchAttr( ["--ratio"], float, default=2.45, help="Aspect ratio of the font" ) @cli.positional(cli.ExistingFile) def main(self, filename): size = None if self.size: size = map(int, self.size.split("x")) Image(size, self.ratio).show(filename, self.double) if __name__ == "__main__": ShowImageApp.run() plumbum-1.8.3/plumbum/cli/progress.py0000644000000000000000000002023414613634536014614 0ustar00""" Progress bar ------------ """ import datetime import sys import warnings from abc import ABC, abstractmethod from plumbum.cli.termsize import get_terminal_size class ProgressBase(ABC): """Base class for progress bars. Customize for types of progress bars. :param iterator: The iterator to wrap with a progress bar :param length: The length of the iterator (will use ``__len__`` if None) :param timer: Try to time the completion status of the iterator :param body: True if the slow portion occurs outside the iterator (in a loop, for example) :param has_output: True if the iteration body produces output to the screen (forces rewrite off) :param clear: Clear the progress bar afterwards, if applicable. """ def __init__( self, iterator=None, length=None, timer=True, body=False, has_output=False, clear=True, ): if length is None: length = len(iterator) elif iterator is None: iterator = range(length) elif length is None and iterator is None: raise TypeError("Expected either an iterator or a length") self.length = length self.iterator = iterator self.timer = timer self.body = body self.has_output = has_output self.clear = clear def __len__(self): return self.length def __iter__(self): self.start() return self @abstractmethod def start(self): """This should initialize the progress bar and the iterator""" self.iter = iter(self.iterator) self.value = -1 if self.body else 0 self._start_time = datetime.datetime.now() def __next__(self): try: rval = next(self.iter) self.increment() except StopIteration: self.done() raise return rval def next(self): return next(self) @property def value(self): """This is the current value, as a property so setting it can be customized""" return self._value @value.setter def value(self, val): self._value = val @abstractmethod def display(self): """Called to update the progress bar""" def increment(self): """Sets next value and displays the bar""" self.value += 1 self.display() def time_remaining(self): """Get the time remaining for the progress bar, guesses""" if self.value < 1: return None, None elapsed_time = datetime.datetime.now() - self._start_time time_each = ( elapsed_time.days * 24 * 60 * 60 + elapsed_time.seconds + elapsed_time.microseconds / 1000000.0 ) / self.value time_remaining = time_each * (self.length - self.value) return elapsed_time, datetime.timedelta(0, time_remaining, 0) def str_time_remaining(self): """Returns a string version of time remaining""" if self.value < 1: return "Starting... " elapsed_time, time_remaining = list(map(str, self.time_remaining())) completed = elapsed_time.split(".")[0] remaining = time_remaining.split(".")[0] return f"{completed} completed, {remaining} remaining" @abstractmethod def done(self): """Is called when the iterator is done.""" @classmethod def range(cls, *value, **kargs): """Fast shortcut to create a range based progress bar, assumes work done in body""" return cls(range(*value), body=True, **kargs) @classmethod def wrap(cls, iterator, length=None, **kargs): """Shortcut to wrap an iterator that does not do all the work internally""" return cls(iterator, length, body=True, **kargs) class Progress(ProgressBase): def start(self): super().start() self.display() def done(self): self.value = self.length self.display() if self.clear and not self.has_output: sys.stdout.write("\r" + len(str(self)) * " " + "\r") else: sys.stdout.write("\n") sys.stdout.flush() def __str__(self): width = get_terminal_size(default=(0, 0))[0] if self.length == 0: self.width = 0 return "0/0 complete" percent = max(self.value, 0) / self.length ending = " " + ( self.str_time_remaining() if self.timer else f"{self.value} of {self.length} complete" ) if width - len(ending) < 10 or self.has_output: self.width = 0 if self.timer: return f"{percent:.0%} complete: {self.str_time_remaining()}" return f"{percent:.0%} complete" self.width = width - len(ending) - 2 - 1 nstars = int(percent * self.width) pbar = "[" + "*" * nstars + " " * (self.width - nstars) + "]" + ending str_percent = f" {percent:.0%} " return ( pbar[: self.width // 2 - 2] + str_percent + pbar[self.width // 2 + len(str_percent) - 2 :] ) def display(self): disptxt = str(self) if self.width == 0 or self.has_output: sys.stdout.write(disptxt + "\n") else: sys.stdout.write("\r") sys.stdout.write(disptxt) sys.stdout.flush() class ProgressIPy(ProgressBase): # pragma: no cover HTMLBOX = '
{0}
' def __init__(self, *args, **kargs): # Ipython gives warnings when using widgets about the API potentially changing with warnings.catch_warnings(): warnings.simplefilter("ignore") try: from ipywidgets import HTML, HBox, IntProgress except ImportError: # Support IPython < 4.0 from IPython.html.widgets import HTML, HBox, IntProgress super().__init__(*args, **kargs) self.prog = IntProgress(max=self.length) self._label = HTML() self._box = HBox((self.prog, self._label)) def start(self): from IPython.display import display display(self._box) super().start() @property def value(self): """This is the current value, -1 allowed (automatically fixed for display)""" return self._value @value.setter def value(self, val): self._value = val self.prog.value = max(val, 0) self.prog.description = f"{self.value / self.length:.2%}" if self.timer and val > 0: self._label.value = self.HTMLBOX.format(self.str_time_remaining()) def display(self): pass def done(self): if self.clear: self._box.close() class ProgressAuto(ProgressBase): """Automatically selects the best progress bar (IPython HTML or text). Does not work with qtconsole (as that is correctly identified as identical to notebook, since the kernel is the same); it will still iterate, but no graphical indication will be displayed. :param iterator: The iterator to wrap with a progress bar :param length: The length of the iterator (will use ``__len__`` if None) :param timer: Try to time the completion status of the iterator :param body: True if the slow portion occurs outside the iterator (in a loop, for example) """ 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__ # noqa: B018 try: from traitlets import TraitError except ImportError: # Support for IPython < 4.0 from IPython.utils.traitlets import TraitError try: return ProgressIPy(*args, **kargs) except TraitError: raise NameError() from None except (NameError, ImportError): return Progress(*args, **kargs) ProgressAuto.register(ProgressIPy) ProgressAuto.register(Progress) def main(): import time tst = Progress.range(20) for _ in tst: time.sleep(1) if __name__ == "__main__": main() plumbum-1.8.3/plumbum/cli/switches.py0000644000000000000000000005112114613634536014600 0ustar00import collections.abc import contextlib import inspect from abc import ABC, abstractmethod from typing import Callable, Generator, List, Union from plumbum import local from plumbum.cli.i18n import get_translation_for from plumbum.lib import getdoc _translation = get_translation_for(__name__) _, ngettext = _translation.gettext, _translation.ngettext class SwitchError(Exception): """A general switch related-error (base class of all other switch errors)""" class PositionalArgumentsError(SwitchError): """Raised when an invalid number of positional arguments has been given""" class SwitchCombinationError(SwitchError): """Raised when an invalid combination of switches has been given""" class UnknownSwitch(SwitchError): """Raised when an unrecognized switch has been given""" class MissingArgument(SwitchError): """Raised when a switch requires an argument, but one was not provided""" class MissingMandatorySwitch(SwitchError): """Raised when a mandatory switch has not been given""" class WrongArgumentType(SwitchError): """Raised when a switch expected an argument of some type, but an argument of a wrong type has been given""" class SubcommandError(SwitchError): """Raised when there's something wrong with sub-commands""" # =================================================================================================== # The switch decorator # =================================================================================================== class SwitchInfo: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) def switch( names, argtype=None, argname=None, list=False, # pylint: disable=redefined-builtin mandatory=False, requires=(), excludes=(), help=None, # pylint: disable=redefined-builtin overridable=False, group="Switches", envname=None, ): """ A decorator that exposes functions as command-line switches. Usage:: class MyApp(Application): @switch(["-l", "--log-to-file"], argtype = str) def log_to_file(self, filename): handler = logging.FileHandler(filename) logger.addHandler(handler) @switch(["--verbose"], excludes=["--terse"], requires=["--log-to-file"]) def set_debug(self): logger.setLevel(logging.DEBUG) @switch(["--terse"], excludes=["--verbose"], requires=["--log-to-file"]) def set_terse(self): logger.setLevel(logging.WARNING) :param names: The name(s) under which the function is reachable; it can be a string or a list of string, but at least one name is required. There's no need to prefix the name with ``-`` or ``--`` (this is added automatically), but it can be used for clarity. Single-letter names are prefixed by ``-``, while longer names are prefixed by ``--`` :param envname: Name of environment variable to extract value from, as alternative to argv :param argtype: If this function takes an argument, you need to specify its type. The default is ``None``, which means the function takes no argument. The type is more of a "validator" than a real type; it can be any callable object that raises a ``TypeError`` if the argument is invalid, or returns an appropriate value on success. If the user provides an invalid value, :func:`plumbum.cli.WrongArgumentType` :param argname: The name of the argument; if ``None``, the name will be inferred from the function's signature :param list: Whether or not this switch can be repeated (e.g. ``gcc -I/lib -I/usr/lib``). If ``False``, only a single occurrence of the switch is allowed; if ``True``, it may be repeated indefinitely. The occurrences are collected into a list, so the function is only called once with the collections. For instance, for ``gcc -I/lib -I/usr/lib``, the function will be called with ``["/lib", "/usr/lib"]``. :param mandatory: Whether or not this switch is mandatory; if a mandatory switch is not given, :class:`MissingMandatorySwitch ` is raised. The default is ``False``. :param requires: A list of switches that this switch depends on ("requires"). This means that it's invalid to invoke this switch without also invoking the required ones. In the example above, it's illegal to pass ``--verbose`` or ``--terse`` without also passing ``--log-to-file``. By default, this list is empty, which means the switch has no prerequisites. If an invalid combination is given, :class:`SwitchCombinationError ` is raised. Note that this list is made of the switch *names*; if a switch has more than a single name, any of its names will do. .. note:: There is no guarantee on the (topological) order in which the actual switch functions will be invoked, as the dependency graph might contain cycles. :param excludes: A list of switches that this switch forbids ("excludes"). This means that it's invalid to invoke this switch if any of the excluded ones are given. In the example above, it's illegal to pass ``--verbose`` along with ``--terse``, as it will result in a contradiction. By default, this list is empty, which means the switch has no prerequisites. If an invalid combination is given, :class:`SwitchCombinationError ` is raised. Note that this list is made of the switch *names*; if a switch has more than a single name, any of its names will do. :param help: The help message (description) for this switch; this description is used when ``--help`` is given. If ``None``, the function's docstring will be used. :param overridable: Whether or not the names of this switch are overridable by other switches. If ``False`` (the default), having another switch function with the same name(s) will cause an exception. If ``True``, this is silently ignored. :param group: The switch's *group*; this is a string that is used to group related switches together when ``--help`` is given. The default group is ``Switches``. :returns: The decorated function (with a ``_switch_info`` attribute) """ if isinstance(names, str): names = [names] names = [n.lstrip("-") for n in names] requires = [n.lstrip("-") for n in requires] excludes = [n.lstrip("-") for n in excludes] def deco(func): if argname is None: argspec = inspect.getfullargspec(func).args argname2 = argspec[1] if len(argspec) == 2 else _("VALUE") else: argname2 = argname help2 = getdoc(func) if help is None else help if not help2: help2 = str(func) func._switch_info = SwitchInfo( names=names, envname=envname, argtype=argtype, list=list, func=func, mandatory=mandatory, overridable=overridable, group=group, requires=requires, excludes=excludes, argname=argname2, help=help2, ) return func return deco def autoswitch(*args, **kwargs): """A decorator that exposes a function as a switch, "inferring" the name of the switch from the function's name (converting to lower-case, and replacing underscores with hyphens). The arguments are the same as for :func:`switch `.""" def deco(func): return switch(func.__name__.replace("_", "-"), *args, **kwargs)(func) return deco # =================================================================================================== # Switch Attributes # =================================================================================================== class SwitchAttr: """ A switch that stores its result in an attribute (descriptor). Usage:: class MyApp(Application): logfile = SwitchAttr(["-f", "--log-file"], str) def main(self): if self.logfile: open(self.logfile, "w") :param names: The switch names :param argtype: The switch argument's (and attribute's) type :param default: The attribute's default value (``None``) :param argname: The switch argument's name (default is ``"VALUE"``) :param kwargs: Any of the keyword arguments accepted by :func:`switch ` """ ATTR_NAME = "__plumbum_switchattr_dict__" VALUE = _("VALUE") def __init__( self, names, argtype=str, default=None, list=False, # pylint: disable=redefined-builtin argname=VALUE, **kwargs, ): # Setting to prevent the help message from showing SwitchAttr's docstring self.__doc__ = "Sets an attribute" if default and argtype is not None: defaultmsg = _("; the default is {0}").format(default) if "help" in kwargs: kwargs["help"] += defaultmsg else: kwargs["help"] = defaultmsg.lstrip("; ") switch(names, argtype=argtype, argname=argname, list=list, **kwargs)(self) listtype = type([]) if list: if default is None: self._default_value = [] elif isinstance(default, (tuple, listtype)): self._default_value = listtype(default) else: self._default_value = [default] else: self._default_value = default def __call__(self, inst, val): self.__set__(inst, val) def __get__(self, inst, cls): if inst is None: return self return getattr(inst, self.ATTR_NAME, {}).get(self, self._default_value) def __set__(self, inst, val): if inst is None: raise AttributeError("cannot set an unbound SwitchAttr") if not hasattr(inst, self.ATTR_NAME): setattr(inst, self.ATTR_NAME, {self: val}) else: getattr(inst, self.ATTR_NAME)[self] = val class Flag(SwitchAttr): """A specialized :class:`SwitchAttr ` for boolean flags. If the flag is not given, the value of this attribute is ``default``; if it is given, the value changes to ``not default``. Usage:: class MyApp(Application): verbose = Flag(["-v", "--verbose"], help = "If given, I'll be very talkative") :param names: The switch names :param default: The attribute's initial value (``False`` by default) :param kwargs: Any of the keyword arguments accepted by :func:`switch `, except for ``list`` and ``argtype``. """ def __init__(self, names, default=False, **kwargs): SwitchAttr.__init__( self, names, argtype=None, default=default, list=False, **kwargs ) def __call__(self, inst): self.__set__(inst, not self._default_value) class CountOf(SwitchAttr): """A specialized :class:`SwitchAttr ` that counts the number of occurrences of the switch in the command line. Usage:: class MyApp(Application): verbosity = CountOf(["-v", "--verbose"], help = "The more, the merrier") If ``-v -v -vv`` is given in the command-line, it will result in ``verbosity = 4``. :param names: The switch names :param default: The default value (0) :param kwargs: Any of the keyword arguments accepted by :func:`switch `, except for ``list`` and ``argtype``. """ def __init__(self, names, default=0, **kwargs): SwitchAttr.__init__( self, names, argtype=None, default=default, list=True, **kwargs ) self._default_value = default # issue #118 def __call__(self, inst, v): self.__set__(inst, len(v)) # =================================================================================================== # Decorator for function that adds argument checking # =================================================================================================== class positional: """ Runs a validator on the main function for a class. This should be used like this:: class MyApp(cli.Application): @cli.positional(cli.Range(1,10), cli.ExistingFile) def main(self, x, *f): # x is a range, f's are all ExistingFile's) Or, Python 3 only:: class MyApp(cli.Application): def main(self, x : cli.Range(1,10), *f : cli.ExistingFile): # x is a range, f's are all ExistingFile's) If you do not want to validate on the annotations, use this decorator ( even if empty) to override annotation validation. Validators should be callable, and should have a ``.choices()`` function with possible choices. (For future argument completion, for example) Default arguments do not go through the validator. #TODO: Check with MyPy """ def __init__(self, *args, **kargs): self.args = args self.kargs = kargs def __call__(self, function): m = inspect.getfullargspec(function) args_names = list(m.args[1:]) positional_list = [None] * len(args_names) varargs = None for i in range(min(len(positional_list), len(self.args))): positional_list[i] = self.args[i] if len(args_names) + 1 == len(self.args): varargs = self.args[-1] # All args are positional, so convert kargs to positional for item, value in self.kargs.items(): if item == m.varargs: varargs = value else: positional_list[args_names.index(item)] = value function.positional = positional_list function.positional_varargs = varargs return function class Validator(ABC): __slots__ = () @abstractmethod def __call__(self, obj): "Must be implemented for a Validator to work" # 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() def __repr__(self): """If not overridden, will print the slots as args""" slots = {} for cls in self.__mro__: for prop in getattr(cls, "__slots__", ()): if prop[0] != "_": slots[prop] = getattr(self, prop) mystrs = (f"{name} = {value}" for name, value in slots.items()) mystrs_str = ", ".join(mystrs) return f"{self.__class__.__name__}({mystrs_str})" # =================================================================================================== # Switch type validators # =================================================================================================== class Range(Validator): """ A switch-type validator that checks for the inclusion of a value in a certain range. Usage:: class MyApp(Application): age = SwitchAttr(["--age"], Range(18, 120)) :param start: The minimal value :param end: The maximal value """ __slots__ = ("start", "end") def __init__(self, start, end): self.start = start self.end = end def __repr__(self): return f"[{self.start:d}..{self.end:d}]" def __call__(self, obj): obj = int(obj) if obj < self.start or obj > self.end: raise ValueError( _("Not in range [{0:d}..{1:d}]").format(self.start, self.end) ) return obj def choices(self, partial=""): # noqa: ARG002 # TODO: Add partial handling return set(range(self.start, self.end + 1)) class Set(Validator): """ A switch-type validator that checks that the value is contained in a defined set of values. Usage:: class MyApp(Application): mode = SwitchAttr(["--mode"], Set("TCP", "UDP", case_sensitive = False)) num = SwitchAttr(["--num"], Set("MIN", "MAX", int, csv = True)) :param values: The set of values (strings), or other callable validators, or types, or any other object that can be compared to a string. :param case_sensitive: A keyword argument that indicates whether to use case-sensitive comparison or not. The default is ``False`` :param csv: splits the input as a comma-separated-value before validating and returning a list. Accepts ``True``, ``False``, or a string for the separator :param all_markers: When a user inputs any value from this set, all values are iterated over. Something like {"*", "all"} would be a potential setting for this option. """ def __init__( self, *values: Union[str, Callable[[str], str]], case_sensitive: bool = False, csv: Union[bool, str] = False, all_markers: "collections.abc.Set[str]" = frozenset(), ) -> None: self.case_sensitive = case_sensitive if isinstance(csv, bool): self.csv = "," if csv else "" else: self.csv = csv self.values = values self.all_markers = all_markers def __repr__(self): items = ", ".join(v if isinstance(v, str) else v.__name__ for v in self.values) return f"{{{items}}}" def _call_iter( self, value: str, check_csv: bool = True ) -> Generator[str, None, None]: if self.csv and check_csv: for v in value.split(self.csv): yield from self._call_iter(v.strip(), check_csv=False) if not self.case_sensitive: value = value.lower() for opt in self.values: if isinstance(opt, str): if not self.case_sensitive: opt = opt.lower() # noqa: PLW2901 if opt == value or value in self.all_markers: yield opt # always return original value continue with contextlib.suppress(ValueError): yield opt(value) def __call__(self, value: str, check_csv: bool = True) -> Union[str, List[str]]: items = list(self._call_iter(value, check_csv)) if not items: msg = f"Invalid value: {value} (Expected one of {self.values})" raise ValueError(msg) if self.csv and check_csv or len(items) > 1: return items return items[0] def choices(self, partial=""): choices = {opt if isinstance(opt, str) else f"({opt})" for opt in self.values} choices |= self.all_markers if partial: return {opt for opt in choices if opt.lower().startswith(partial)} return choices CSV = Set(str, csv=True) class Predicate: """A wrapper for a single-argument function with pretty printing""" def __init__(self, func): self.func = func def __str__(self): return self.func.__name__ def __call__(self, val): return self.func(val) # pylint: disable-next=no-self-use def choices(self, partial=""): # noqa: ARG002 return set() @Predicate def ExistingDirectory(val): """A switch-type validator that ensures that the given argument is an existing directory""" p = local.path(val) if not p.is_dir(): raise ValueError(_("{0} is not a directory").format(val)) return p @Predicate def MakeDirectory(val): p = local.path(val) if p.is_file(): raise ValueError(f"{val} is a file, should be nonexistent, or a directory") if not p.exists(): p.mkdir() return p @Predicate def ExistingFile(val): """A switch-type validator that ensures that the given argument is an existing file""" p = local.path(val) if not p.is_file(): raise ValueError(_("{0} is not a file").format(val)) return p @Predicate def NonexistentPath(val): """A switch-type validator that ensures that the given argument is a nonexistent path""" p = local.path(val) if p.exists(): raise ValueError(_("{0} already exists").format(val)) return p plumbum-1.8.3/plumbum/cli/terminal.py0000644000000000000000000001606614613634536014573 0ustar00""" Terminal-related utilities -------------------------- """ import contextlib import os import sys from typing import List, Optional from plumbum import local from .progress import Progress from .termsize import get_terminal_size __all__ = [ "readline", "ask", "choose", "prompt", "get_terminal_size", "Progress", "get_terminal_size", ] def __dir__() -> List[str]: return __all__ def readline(message: str = "") -> str: """Gets a line of input from the user (stdin)""" sys.stdout.write(message) sys.stdout.flush() return sys.stdin.readline() def ask(question: str, default: Optional[bool] = None) -> bool: """ Presents the user with a yes/no question. :param question: The question to ask :param default: If ``None``, the user must answer. If ``True`` or ``False``, lack of response is interpreted as the default option :returns: the user's choice """ question = question.rstrip().rstrip("?").rstrip() + "?" if default is None: question += " (y/n) " elif default: question += " [Y/n] " else: question += " [y/N] " while True: try: answer = readline(question).strip().lower() except EOFError: answer = None if answer in {"y", "yes"}: return True if answer in {"n", "no"}: return False if not answer and default is not None: return default sys.stdout.write("Invalid response, please try again\n") def choose(question, options, default=None): """Prompts the user with a question and a set of options, from which the user needs to choose. :param question: The question to ask :param options: A set of options. It can be a list (of strings or two-tuples, mapping text to returned-object) or a dict (mapping text to returned-object).`` :param default: If ``None``, the user must answer. Otherwise, lack of response is interpreted as this answer :returns: The user's choice Example:: ans = choose("What is your favorite color?", ["blue", "yellow", "green"], default = "yellow") # `ans` will be one of "blue", "yellow" or "green" ans = choose("What is your favorite color?", {"blue" : 0x0000ff, "yellow" : 0xffff00 , "green" : 0x00ff00}, default = 0x00ff00) # this will display "blue", "yellow" and "green" but return a numerical value """ if hasattr(options, "items"): options = options.items() sys.stdout.write(question.rstrip() + "\n") choices = {} defindex = None for i, item in enumerate(options, 1): if isinstance(item, (tuple, list)) and len(item) == 2: text = item[0] val = item[1] else: text = item val = item choices[i] = val if default is not None and default == val: defindex = i sys.stdout.write(f"({i}) {text}\n") if default is not None: msg = f"Choice [{default}]: " if defindex is None else f"Choice [{defindex}]: " else: msg = "Choice: " while True: try: choice = readline(msg).strip() except EOFError: choice = "" if not choice and default: return default try: choice = int(choice) if choice not in choices: raise ValueError() except ValueError: sys.stdout.write("Invalid choice, please try again\n") continue return choices[choice] def prompt( question, type=str, # pylint: disable=redefined-builtin default=NotImplemented, validator=lambda _: True, ): """ Presents the user with a validated question, keeps asking if validation does not pass. :param question: The question to ask :param type: The type of the answer, defaults to str :param default: The default choice :param validator: An extra validator called after type conversion, can raise ValueError or return False to trigger a retry. :returns: the user's choice """ question = question.rstrip(" \t:") if default is not NotImplemented: question += f" [{default}]" question += ": " while True: try: ans = readline(question).strip() except EOFError: ans = "" if not ans: if default is not NotImplemented: # sys.stdout.write("\b%s\n" % (default,)) return default continue try: ans = type(ans) except (TypeError, ValueError) as ex: sys.stdout.write(f"Invalid value ({ex}), please try again\n") continue try: valid = validator(ans) except ValueError as ex: sys.stdout.write(f"{ex}, please try again\n") continue if not valid: sys.stdout.write("Value not in specified range, please try again\n") continue return ans def hexdump(data_or_stream, bytes_per_line=16, aggregate=True): """Convert the given bytes (or a stream with a buffering ``read()`` method) to hexdump-formatted lines, with possible aggregation of identical lines. Returns a generator of formatted lines. """ if hasattr(data_or_stream, "read"): def read_chunk(): while True: buf = data_or_stream.read(bytes_per_line) if not buf: break yield buf else: def read_chunk(): for i in range(0, len(data_or_stream), bytes_per_line): yield data_or_stream[i : i + bytes_per_line] prev = None skipped = False for i, chunk in enumerate(read_chunk()): hexd = " ".join(f"{ord(ch):02x}" for ch in chunk) text = "".join(ch if 32 <= ord(ch) < 127 else "." for ch in chunk) if aggregate and prev == chunk: skipped = True continue prev = chunk if skipped: yield "*" hexd_ljust = hexd.ljust(bytes_per_line * 3, " ") yield f"{i*bytes_per_line:06x} | {hexd_ljust}| {text}" skipped = False def pager(rows, pagercmd=None): # pragma: no cover """Opens a pager (e.g., ``less``) to display the given text. Requires a terminal. :param rows: a ``bytes`` or a list/iterator of "rows" (``bytes``) :param pagercmd: the pager program to run. Defaults to ``less -RSin`` """ if not pagercmd: pagercmd = local["less"]["-RSin"] if hasattr(rows, "splitlines"): rows = rows.splitlines() pg = pagercmd.popen(stdout=None, stderr=None) try: for row in rows: line = f"{row}\n" try: pg.stdin.write(line) pg.stdin.flush() except OSError: break pg.stdin.close() pg.wait() finally: with contextlib.suppress(Exception): rows.close() if pg and pg.poll() is None: with contextlib.suppress(Exception): pg.terminate() os.system("reset") plumbum-1.8.3/plumbum/cli/termsize.py0000644000000000000000000000600714613634536014614 0ustar00""" Terminal size utility --------------------- """ import contextlib import os import platform import warnings from struct import Struct from typing import Optional, Tuple from plumbum import local def get_terminal_size(default: Tuple[int, int] = (80, 25)) -> Tuple[int, int]: """ Get width and height of console; works on linux, os x, windows and cygwin Adapted from https://gist.github.com/jtriley/1108174 Originally from: http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python """ current_os = platform.system() if current_os == "Windows": # pragma: no cover size = _get_terminal_size_windows() if not size: # needed for window's python in cygwin's xterm! size = _get_terminal_size_tput() elif current_os in ("Linux", "Darwin", "FreeBSD", "SunOS") or current_os.startswith( "CYGWIN" ): size = _get_terminal_size_linux() else: # pragma: no cover warnings.warn( "Plumbum does not know the type of the current OS for term size, defaulting to UNIX", stacklevel=2, ) size = _get_terminal_size_linux() # 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 def _get_terminal_size_windows(): # pragma: no cover try: from ctypes import create_string_buffer, windll STDERR_HANDLE = -12 h = windll.kernel32.GetStdHandle(STDERR_HANDLE) csbi_struct = Struct("hhhhHhhhhhh") csbi = create_string_buffer(csbi_struct.size) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) if res: _, _, _, _, _, left, top, right, bottom, _, _ = csbi_struct.unpack(csbi.raw) return right - left + 1, bottom - top + 1 return None except Exception: return None def _get_terminal_size_tput(): # pragma: no cover # get terminal width # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window try: tput = local["tput"] cols = int(tput("cols")) rows = int(tput("lines")) return (cols, rows) except Exception: return None def _ioctl_GWINSZ(fd: int) -> Optional[Tuple[int, int]]: yx = Struct("hh") try: import fcntl import termios # TODO: Clean this up. Problems could be hidden by the broad except. return yx.unpack(fcntl.ioctl(fd, termios.TIOCGWINSZ, b"1234")) except Exception: return None def _get_terminal_size_linux() -> Optional[Tuple[int, int]]: cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) if not cr: with contextlib.suppress(Exception): fd = os.open(os.ctermid(), os.O_RDONLY) cr = _ioctl_GWINSZ(fd) os.close(fd) if not cr: try: cr = (int(os.environ["LINES"]), int(os.environ["COLUMNS"])) except Exception: return None return cr[1], cr[0] plumbum-1.8.3/plumbum/cli/i18n/de.po0000644000000000000000000001514214613634536014107 0ustar00# German Translations for PACKAGE package. # Deutsche Übersetzung für PACKAGE paket. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Christoph Hasse , 2017. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-10-05 22:39-0400\n" "PO-Revision-Date: 2017-11-02 15:04+0200\n" "Last-Translator: Christoph Hasse \n" "Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plumbum/cli/application.py:69 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Unterbefehl({self.name}, {self.subapplication})" #: plumbum/cli/application.py:73 msgid "Switches" msgstr "Optionen" #: plumbum/cli/application.py:73 msgid "Meta-switches" msgstr "Meta-optionen" #: plumbum/cli/application.py:163 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "siehe '{parent} {sub} --help' für mehr Informationen" #: plumbum/cli/application.py:220 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "Unterbefehle können nicht mit '-' beginnen" #: plumbum/cli/application.py:238 #, fuzzy, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Option {name} ist bereits definiert und nicht überschreibbar" #: plumbum/cli/application.py:343 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:348 plumbum/cli/application.py:373 #: plumbum/cli/application.py:389 #, python-brace-format msgid "Unknown switch {0}" msgstr "Unbekannte Option {0}" #: plumbum/cli/application.py:353 plumbum/cli/application.py:362 #: plumbum/cli/application.py:381 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Option {0} benötigt ein Argument" #: plumbum/cli/application.py:401 #, python-brace-format msgid "Switch {0} already given" msgstr "Option {0} bereits gegeben" #: plumbum/cli/application.py:403 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Option {0} bereits gegeben({1} ist äquivalent)" #: plumbum/cli/application.py:451 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Argument von {name} sollte {argtype} sein, nicht {val|1}:\n" " {ex!r}" #: plumbum/cli/application.py:470 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Option {0} ist notwendig" #: plumbum/cli/application.py:490 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "Gegeben {0}, werden die folgenden vermisst {1}" #: plumbum/cli/application.py:498 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "Gegeben {0}, sind die folgenden ungültig {1}" #: plumbum/cli/application.py:515 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Erwarte mindestens {0} positionelles Argument, erhalte {1}" msgstr[1] "Erwarte mindestens {0} positionelle Argumente, erhalte {1}" #: plumbum/cli/application.py:523 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Erwarte höchstens {0} positionelles Argument, erhalte {0}" msgstr[1] "Erwarte höchstens {0} positionelle Argumente, erhalte {0}" #: plumbum/cli/application.py:624 #, python-brace-format msgid "Error: {0}" msgstr "Fehler: {0}" #: plumbum/cli/application.py:625 plumbum/cli/application.py:711 #: plumbum/cli/application.py:716 msgid "------" msgstr "------" #: plumbum/cli/application.py:694 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Option {0} muss eine Sequenz sein (iterierbar)" #: plumbum/cli/application.py:699 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Option {0} ist ein boolescher Wert" #: plumbum/cli/application.py:710 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Unbekannter Unterbefehl '{0}'" #: plumbum/cli/application.py:715 msgid "No sub-command given" msgstr "Kein Unterbefehl gegeben" #: plumbum/cli/application.py:721 msgid "main() not implemented" msgstr "main() nicht implementiert" #: plumbum/cli/application.py:734 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Druckt die Hilfetexte aller Unterbefehle und terminiert" #: plumbum/cli/application.py:754 msgid "Prints this help message and quits" msgstr "Druckt den Hilfetext und terminiert" #: plumbum/cli/application.py:877 msgid "Usage:" msgstr "Gebrauch:" #: plumbum/cli/application.py:883 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [OPTIONEN] [UNTERBEFEHL [OPTIONEN]] {tailargs}\n" #: plumbum/cli/application.py:886 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [OPTIONEN] {tailargs}\n" #: plumbum/cli/application.py:936 msgid "; may be given multiple times" msgstr "; kann mehrmals angegeben werden" #: plumbum/cli/application.py:938 msgid "; required" msgstr "; benötigt" #: plumbum/cli/application.py:940 #, python-brace-format msgid "; requires {0}" msgstr "; benötigt {0}" #: plumbum/cli/application.py:947 #, python-brace-format msgid "; excludes {0}" msgstr "; schließt {0} aus" #: plumbum/cli/application.py:966 #, fuzzy msgid "Sub-commands:" msgstr "Unterbefehle:" #: plumbum/cli/application.py:1014 msgid "Prints the program's version and quits" msgstr "Druckt die Programmversion und terminiert" #: plumbum/cli/application.py:1019 msgid "(version not set)" msgstr "(Version nicht gesetzt)" #: plumbum/cli/switches.py:167 plumbum/cli/switches.py:225 msgid "VALUE" msgstr "WERT" #: plumbum/cli/switches.py:238 #, python-brace-format msgid "; the default is {0}" msgstr "; der Standard ist {0}" #: plumbum/cli/switches.py:437 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Nicht im Wertebereich [{0:d}..{1:d}]" #: plumbum/cli/switches.py:546 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} ist kein Ordner" #: plumbum/cli/switches.py:565 #, python-brace-format msgid "{0} is not a file" msgstr "{0} ist keine Datei" #: plumbum/cli/switches.py:574 #, python-brace-format msgid "{0} already exists" msgstr "{0} existiert bereits" #, python-brace-format #~ msgid "got unexpected keyword argument(s): {0}" #~ msgstr "Unerwartete(s) Argument(e) erhalten: {0}" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "Erwartet einen von {0}" plumbum-1.8.3/plumbum/cli/i18n/fr.po0000644000000000000000000001511614613634536014127 0ustar00# French Translations for PACKAGE package. # Traduction francaise du paquet PACKAGE. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Joel Closier , 2017. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-10-05 22:39-0400\n" "PO-Revision-Date: 2017-10-14 15:04+0200\n" "Last-Translator: Joel Closier \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plumbum/cli/application.py:69 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Sous-commande({self.name}, {self.subapplication})" #: plumbum/cli/application.py:73 msgid "Switches" msgstr "Options" #: plumbum/cli/application.py:73 msgid "Meta-switches" msgstr "Meta-options" #: plumbum/cli/application.py:163 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "voir '{parent} {sub} --help' pour plus d'information" #: plumbum/cli/application.py:220 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "le nom des Sous-commandes ne peut pas commencer avec '-' " #: plumbum/cli/application.py:238 #, fuzzy, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Option {name} est déjà définie et ne peut pas être sur-écrite" #: plumbum/cli/application.py:343 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:348 plumbum/cli/application.py:373 #: plumbum/cli/application.py:389 #, python-brace-format msgid "Unknown switch {0}" msgstr "Option inconnue {0}" #: plumbum/cli/application.py:353 plumbum/cli/application.py:362 #: plumbum/cli/application.py:381 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Option {0} nécessite un argument" #: plumbum/cli/application.py:401 #, python-brace-format msgid "Switch {0} already given" msgstr "Option {0} déjà donnée" #: plumbum/cli/application.py:403 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Option {0} déjà donnée ({1} est équivalent)" #: plumbum/cli/application.py:451 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Argument de {name} doit être {argtype} , et non {val!r}:\n" " {ex!r}" #: plumbum/cli/application.py:470 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Option {0} obligatoire" #: plumbum/cli/application.py:490 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "Etant donné {0}, ce qui suit est manquant {1}" #: plumbum/cli/application.py:498 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "Etant donné {0}, ce qui suit est invalide {1}" #: plumbum/cli/application.py:515 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Au moins {0} argument de position attendu, reçu {0}" msgstr[1] "Au moins {0} arguments de position, reçu {0}" #: plumbum/cli/application.py:523 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Au plus {0} argument de position attendu, reçu {0}" msgstr[1] "Au plus {0} arguments de position, reçu {0}" #: plumbum/cli/application.py:624 #, python-brace-format msgid "Error: {0}" msgstr "Erreur: {0}" #: plumbum/cli/application.py:625 plumbum/cli/application.py:711 #: plumbum/cli/application.py:716 msgid "------" msgstr "------" #: plumbum/cli/application.py:694 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Option {0} doit être une séquence (itérable)" #: plumbum/cli/application.py:699 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Option {0} est un booléen" #: plumbum/cli/application.py:710 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Sous-commande inconnue '{0}'" #: plumbum/cli/application.py:715 msgid "No sub-command given" msgstr "Pas de sous-commande donnée" #: plumbum/cli/application.py:721 msgid "main() not implemented" msgstr "main() n'est pas implémenté" #: plumbum/cli/application.py:734 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Imprime les messages d'aide de toutes les sous-commandes et sort" #: plumbum/cli/application.py:754 msgid "Prints this help message and quits" msgstr "Imprime ce message d'aide et sort" #: plumbum/cli/application.py:877 msgid "Usage:" msgstr "Utilisation:" #: plumbum/cli/application.py:883 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [OPTIONS] [SOUS_COMMANDE [OPTIONS]] {tailargs}\n" #: plumbum/cli/application.py:886 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [OPTIONS] {tailargs}\n" #: plumbum/cli/application.py:936 msgid "; may be given multiple times" msgstr "; peut être fourni plusieurs fois" #: plumbum/cli/application.py:938 msgid "; required" msgstr "; nécessaire" #: plumbum/cli/application.py:940 #, python-brace-format msgid "; requires {0}" msgstr "; nécessite {0}" #: plumbum/cli/application.py:947 #, python-brace-format msgid "; excludes {0}" msgstr "; exclut {0}" #: plumbum/cli/application.py:966 #, fuzzy msgid "Sub-commands:" msgstr "Sous-Commandes:" #: plumbum/cli/application.py:1014 msgid "Prints the program's version and quits" msgstr "Imprime la version du programme et sort" #: plumbum/cli/application.py:1019 msgid "(version not set)" msgstr "(version non définie)" #: plumbum/cli/switches.py:167 plumbum/cli/switches.py:225 msgid "VALUE" msgstr "VALEUR" #: plumbum/cli/switches.py:238 #, python-brace-format msgid "; the default is {0}" msgstr "; la valeur par défaut est {0}" #: plumbum/cli/switches.py:437 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Pas dans la chaîne [{0:d}..{1:d}]" #: plumbum/cli/switches.py:546 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} n'est pas un répertoire" #: plumbum/cli/switches.py:565 #, python-brace-format msgid "{0} is not a file" msgstr "{0} n'est pas un fichier" #: plumbum/cli/switches.py:574 #, python-brace-format msgid "{0} already exists" msgstr "{0} existe déjà" #, python-brace-format #~ msgid "got unexpected keyword argument(s): {0}" #~ msgstr "mot-clé inconnu donné comme argument: {0}" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "un des {0} attendu" plumbum-1.8.3/plumbum/cli/i18n/nl.po0000644000000000000000000001510714613634536014131 0ustar00# Dutch Translations for PACKAGE package. # Nederlandse vertaling voor het PACKAGE pakket. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Roel Aaij , 2017. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-10-05 22:39-0400\n" "PO-Revision-Date: 2017-10-14 15:04+0200\n" "Last-Translator: Roel Aaij \n" "Language: nl_NL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plumbum/cli/application.py:69 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Subopdracht({self.name}, {self.subapplication})" #: plumbum/cli/application.py:73 msgid "Switches" msgstr "Opties" #: plumbum/cli/application.py:73 msgid "Meta-switches" msgstr "Meta-opties" #: plumbum/cli/application.py:163 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "zie '{parent} {sub} --help' voor meer informatie" #: plumbum/cli/application.py:220 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "Namen van subopdrachten mogen niet met '-' beginnen" #: plumbum/cli/application.py:238 #, fuzzy, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Optie {name} is al gedefiniëerd en kan niet worden overschreven" #: plumbum/cli/application.py:343 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:348 plumbum/cli/application.py:373 #: plumbum/cli/application.py:389 #, python-brace-format msgid "Unknown switch {0}" msgstr "Onbekende optie {0}" #: plumbum/cli/application.py:353 plumbum/cli/application.py:362 #: plumbum/cli/application.py:381 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Een argument is vereist bij optie {0}" #: plumbum/cli/application.py:401 #, python-brace-format msgid "Switch {0} already given" msgstr "Optie {0} is al gegeven" #: plumbum/cli/application.py:403 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Optie {0} is al gegeven ({1} is equivalent)" #: plumbum/cli/application.py:451 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Argement van {name} hoort {argtype} te zijn, niet {val|1}:\n" " {ex!r}" #: plumbum/cli/application.py:470 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Optie {0} is verplicht" #: plumbum/cli/application.py:490 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "Gegeven {0}, ontbreken de volgenden {1}" #: plumbum/cli/application.py:498 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "Gegeven {0}, zijn de volgenden ongeldig {1}" #: plumbum/cli/application.py:515 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Verwachtte ten minste {0} positioneel argument, kreeg {1}" msgstr[1] "Verwachtte ten minste {0} positionele argumenten, kreeg {1}" #: plumbum/cli/application.py:523 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Verwachtte hoogstens {0} positioneel argument, kreeg {0}" msgstr[1] "Verwachtte hoogstens {0} positionele argumenten, kreeg {0}" #: plumbum/cli/application.py:624 #, python-brace-format msgid "Error: {0}" msgstr "Fout: {0}" #: plumbum/cli/application.py:625 plumbum/cli/application.py:711 #: plumbum/cli/application.py:716 msgid "------" msgstr "------" #: plumbum/cli/application.py:694 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Optie {0} moet een reeks zijn (itereerbaar object)" #: plumbum/cli/application.py:699 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Optie {0} geeft een waarheidswaarde weer" #: plumbum/cli/application.py:710 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Onbekend subcommando '{0}'" #: plumbum/cli/application.py:715 msgid "No sub-command given" msgstr "Geen subcommando gegeven" #: plumbum/cli/application.py:721 msgid "main() not implemented" msgstr "main() niet geïmplementeerd" #: plumbum/cli/application.py:734 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Druk hulpberichten van alle subcommando's af en beëindig" #: plumbum/cli/application.py:754 msgid "Prints this help message and quits" msgstr "Drukt dit hulpbericht af en beëindig" #: plumbum/cli/application.py:877 msgid "Usage:" msgstr "Gebruik:" #: plumbum/cli/application.py:883 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [OPTIES] [SUBCOMMANDO [OPTIES]] {tailargs}\n" #: plumbum/cli/application.py:886 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [OPTIES] {tailargs}\n" #: plumbum/cli/application.py:936 msgid "; may be given multiple times" msgstr "; kan meerdere keren gegeven worden" #: plumbum/cli/application.py:938 msgid "; required" msgstr "; vereist" #: plumbum/cli/application.py:940 #, python-brace-format msgid "; requires {0}" msgstr "; verseist {0}" #: plumbum/cli/application.py:947 #, python-brace-format msgid "; excludes {0}" msgstr "; sluit {0} uit" #: plumbum/cli/application.py:966 #, fuzzy msgid "Sub-commands:" msgstr "Subcommando's" #: plumbum/cli/application.py:1014 msgid "Prints the program's version and quits" msgstr "Drukt de versie van het programma af en beëindigt" #: plumbum/cli/application.py:1019 msgid "(version not set)" msgstr "(versie niet opgegeven)" #: plumbum/cli/switches.py:167 plumbum/cli/switches.py:225 msgid "VALUE" msgstr "WAARDE" #: plumbum/cli/switches.py:238 #, python-brace-format msgid "; the default is {0}" msgstr "; de standaard is {0}" #: plumbum/cli/switches.py:437 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Niet binnen bereik [{0:d}..{1:d}]" #: plumbum/cli/switches.py:546 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} is geen map" #: plumbum/cli/switches.py:565 #, python-brace-format msgid "{0} is not a file" msgstr "{0} is geen bestand" #: plumbum/cli/switches.py:574 #, python-brace-format msgid "{0} already exists" msgstr "{0} bestaat al" #, python-brace-format #~ msgid "got unexpected keyword argument(s): {0}" #~ msgstr "Onverwacht(e) trefwoord argument(en) gegeven: {0}" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "Verwachtte één van {0}" plumbum-1.8.3/plumbum/cli/i18n/ru.po0000644000000000000000000001760614613634536014154 0ustar00# Russian translations for PACKAGE package # Английские переводы для пакета PACKAGE. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # , 2017. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-10-05 22:39-0400\n" "PO-Revision-Date: 2017-08-14 10:21+0200\n" "Last-Translator: \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: plumbum/cli/application.py:69 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Подкоманда({self.name}, {self.subapplication})" #: plumbum/cli/application.py:73 msgid "Switches" msgstr "Опции" #: plumbum/cli/application.py:73 msgid "Meta-switches" msgstr "Мета-опции" #: plumbum/cli/application.py:163 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "вызовите '{parent} {sub} --help' для более полной справки" #: plumbum/cli/application.py:220 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "Имена подкомманд не могут начинаться с '-'" #: plumbum/cli/application.py:238 #, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Опция {name} уже определена и не может быть переопределена" #: plumbum/cli/application.py:343 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:348 plumbum/cli/application.py:373 #: plumbum/cli/application.py:389 #, python-brace-format msgid "Unknown switch {0}" msgstr "Неизестная опция {0}" #: plumbum/cli/application.py:353 plumbum/cli/application.py:362 #: plumbum/cli/application.py:381 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Для опции {0} необходим аргумент" #: plumbum/cli/application.py:401 #, python-brace-format msgid "Switch {0} already given" msgstr "Опция {0} уже была передана" #: plumbum/cli/application.py:403 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Опция {0} уже была передана (эквивалентна {1})" #: plumbum/cli/application.py:451 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Аргумент опции {name} должен быть типа {argtype}, но не {val!r}:\n" " {ex!r}" #: plumbum/cli/application.py:470 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Опция {0} обязательна" #: plumbum/cli/application.py:490 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "При передаче {0}, необходимо также указать {1}" #: plumbum/cli/application.py:498 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "При передаче {0}, нельзя указать {1}" #: plumbum/cli/application.py:515 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Ожидая как минимум {0} позиционный аргумент, получено {1}" msgstr[1] "Ожидая как минимум {0} позиционных аргумента, получено {1}" msgstr[2] "Ожидая как минимум {0} позиционных аргументов, получено {1}" #: plumbum/cli/application.py:523 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Ожидая как максимум {0} позиционный аргумент, получено {1}" msgstr[1] "Ожидая как максимум {0} позиционных аргумента, получено {1}" msgstr[2] "Ожидая как максимум {0} позиционных аргументов, получено {1}" #: plumbum/cli/application.py:624 #, python-brace-format msgid "Error: {0}" msgstr "Ошибка: {0}" #: plumbum/cli/application.py:625 plumbum/cli/application.py:711 #: plumbum/cli/application.py:716 msgid "------" msgstr "-------" #: plumbum/cli/application.py:694 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Опция {0} должна быть последовательностью (перечислением)" #: plumbum/cli/application.py:699 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Опция {0} - это булев флаг" #: plumbum/cli/application.py:710 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Неизестная подкоманда '{0}'" #: plumbum/cli/application.py:715 msgid "No sub-command given" msgstr "Подкоманда не задана" #: plumbum/cli/application.py:721 msgid "main() not implemented" msgstr "Функция main() не реализована" #: plumbum/cli/application.py:734 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Печатает помощь по всем подкомандам и выходит" #: plumbum/cli/application.py:754 msgid "Prints this help message and quits" msgstr "Печатает это сообщение и выходит" #: plumbum/cli/application.py:877 msgid "Usage:" msgstr "Использование:" #: plumbum/cli/application.py:883 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [ОПЦИИ] [ПОДКОМАНДА [ОПЦИИ]] {tailargs}\n" #: plumbum/cli/application.py:886 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [ОПЦИИ] {tailargs}\n" #: plumbum/cli/application.py:936 msgid "; may be given multiple times" msgstr "; может быть передана несколько раз" #: plumbum/cli/application.py:938 msgid "; required" msgstr "; обязательная" #: plumbum/cli/application.py:940 #, python-brace-format msgid "; requires {0}" msgstr "; запрашивает {0}" #: plumbum/cli/application.py:947 #, python-brace-format msgid "; excludes {0}" msgstr "; исключает {0}" #: plumbum/cli/application.py:966 #, fuzzy msgid "Sub-commands:" msgstr "Подкоманды:" #: plumbum/cli/application.py:1014 msgid "Prints the program's version and quits" msgstr "Печатает версию этой программы и выходит" #: plumbum/cli/application.py:1019 msgid "(version not set)" msgstr "(версия не задана)" #: plumbum/cli/switches.py:167 plumbum/cli/switches.py:225 msgid "VALUE" msgstr "ЗНАЧЕНИЕ" #: plumbum/cli/switches.py:238 #, python-brace-format msgid "; the default is {0}" msgstr "; по умолчанию - {0}" #: plumbum/cli/switches.py:437 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Не в промежутке [{0:d}..{1:d}]" #: plumbum/cli/switches.py:546 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} - это не папка" #: plumbum/cli/switches.py:565 #, python-brace-format msgid "{0} is not a file" msgstr "{0} - это не файл" #: plumbum/cli/switches.py:574 #, python-brace-format msgid "{0} already exists" msgstr "{0} уже была передана" #, python-brace-format #~ msgid "got unexpected keyword argument(s): {0}" #~ msgstr "получен(ы) неожиданный(е) аргумент(ы) ключ-значение: {0}" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "Ожидался один из {0}" plumbum-1.8.3/plumbum/cli/i18n/de/LC_MESSAGES/plumbum.cli.mo0000644000000000000000000000674114613634536020125 0ustar00&L5|P=Q% D* ofzd(F(o &".!P,i(7JQW)n >% C [ b v   D  u u - .5 d r $ ) # / . /I "y  . !   - C M R 5m    $&#"   % ! {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEmain() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-11-02 15:04+0200 Last-Translator: Christoph Hasse Language: de_DE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {progname} [OPTIONEN] [UNTERBEFEHL [OPTIONEN]] {tailargs} {progname} [OPTIONEN] {tailargs} (Version nicht gesetzt)------; schließt {0} aus; kann mehrmals angegeben werden; benötigt; benötigt {0}; der Standard ist {0}Argument von {name} sollte {argtype} sein, nicht {val|1}: {ex!r}Fehler: {0}Erwarte mindestens {0} positionelles Argument, erhalte {1}Erwarte mindestens {0} positionelle Argumente, erhalte {1}Erwarte höchstens {0} positionelles Argument, erhalte {0}Erwarte höchstens {0} positionelle Argumente, erhalte {0}Gegeben {0}, sind die folgenden ungültig {1}Gegeben {0}, werden die folgenden vermisst {1}Meta-optionenKein Unterbefehl gegebenNicht im Wertebereich [{0:d}..{1:d}]Druckt die Programmversion und terminiertDruckt den Hilfetext und terminiertUnterbefehl({self.name}, {self.subapplication})Option {0} bereits gegebenOption {0} bereits gegeben({1} ist äquivalent)Option {0} ist ein boolescher WertOption {0} ist notwendigOption {0} muss eine Sequenz sein (iterierbar)Option {0} benötigt ein ArgumentOptionenUnbekannter Unterbefehl '{0}'Unbekannte Option {0}Gebrauch:WERTmain() nicht implementiertsiehe '{parent} {sub} --help' für mehr Informationen{0} existiert bereits{0} ist kein Ordner{0} ist keine Dateiplumbum-1.8.3/plumbum/cli/i18n/fr/LC_MESSAGES/plumbum.cli.mo0000644000000000000000000000667214613634536020147 0ustar00&L5|P=Q% D* ofzd(F(o &".!P,i(7JQW)n>$ ; R Y "f   D b `| . . ; H "e ' ! 1  / N i / !      4= r   $&#"   % ! {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEmain() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-10-14 15:04+0200 Last-Translator: Joel Closier Language: fr_FR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {progname} [OPTIONS] [SOUS_COMMANDE [OPTIONS]] {tailargs} {progname} [OPTIONS] {tailargs} (version non définie)------; exclut {0}; peut être fourni plusieurs fois; nécessaire; nécessite {0}; la valeur par défaut est {0}Argument de {name} doit être {argtype} , et non {val!r}: {ex!r}Erreur: {0}Au moins {0} argument de position attendu, reçu {0}Au moins {0} arguments de position, reçu {0}Au plus {0} argument de position attendu, reçu {0}Au plus {0} arguments de position, reçu {0}Etant donné {0}, ce qui suit est invalide {1}Etant donné {0}, ce qui suit est manquant {1}Meta-optionsPas de sous-commande donnéePas dans la chaîne [{0:d}..{1:d}]Imprime la version du programme et sortImprime ce message d'aide et sortSous-commande({self.name}, {self.subapplication})Option {0} déjà donnéeOption {0} déjà donnée ({1} est équivalent)Option {0} est un booléenOption {0} obligatoireOption {0} doit être une séquence (itérable)Option {0} nécessite un argumentOptionsSous-commande inconnue '{0}'Option inconnue {0}Utilisation:VALEURmain() n'est pas implémentévoir '{parent} {sub} --help' pour plus d'information{0} existe déjà{0} n'est pas un répertoire{0} n'est pas un fichierplumbum-1.8.3/plumbum/cli/i18n/nl/LC_MESSAGES/plumbum.cli.mo0000644000000000000000000000666614613634536020154 0ustar00&L5|P=Q% D* ofzd(F(o &".!P,i(7JQW)n:# 2 J Q #a   E u sz + ' B N !g 2 % /  +* (V  2 %    % . 5 0R    $&#"   % ! {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEmain() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-10-14 15:04+0200 Last-Translator: Roel Aaij Language: nl_NL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {progname} [OPTIES] [SUBCOMMANDO [OPTIES]] {tailargs} {progname} [OPTIES] {tailargs} (versie niet opgegeven)------; sluit {0} uit; kan meerdere keren gegeven worden; vereist; verseist {0}; de standaard is {0}Argement van {name} hoort {argtype} te zijn, niet {val|1}: {ex!r}Fout: {0}Verwachtte ten minste {0} positioneel argument, kreeg {1}Verwachtte ten minste {0} positionele argumenten, kreeg {1}Verwachtte hoogstens {0} positioneel argument, kreeg {0}Verwachtte hoogstens {0} positionele argumenten, kreeg {0}Gegeven {0}, zijn de volgenden ongeldig {1}Gegeven {0}, ontbreken de volgenden {1}Meta-optiesGeen subcommando gegevenNiet binnen bereik [{0:d}..{1:d}]Drukt de versie van het programma af en beëindigtDrukt dit hulpbericht af en beëindigSubopdracht({self.name}, {self.subapplication})Optie {0} is al gegevenOptie {0} is al gegeven ({1} is equivalent)Optie {0} geeft een waarheidswaarde weerOptie {0} is verplichtOptie {0} moet een reeks zijn (itereerbaar object)Een argument is vereist bij optie {0}OptiesOnbekend subcommando '{0}'Onbekende optie {0}Gebruik:WAARDEmain() niet geïmplementeerdzie '{parent} {sub} --help' voor meer informatie{0} bestaat al{0} is geen map{0} is geen bestandplumbum-1.8.3/plumbum/cli/i18n/ru/LC_MESSAGES/plumbum.cli.mo0000644000000000000000000001134414613634536020156 0ustar00'T5`=a% %D: fd(V( &".1`,y(4$Yb|)K '  : B @[    j _ 4q : < On&+K<!8^/N+%Bhh9f  r/}#1V0$%$#   '& ! " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitch {name} already defined and is not overridableSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEmain() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-08-14 10:21+0200 Last-Translator: Language-Team: Russian Language: ru MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); {progname} [ОПЦИИ] [ПОДКОМАНДА [ОПЦИИ]] {tailargs} {progname} [ОПЦИИ] {tailargs} (версия не задана)-------; исключает {0}; может быть передана несколько раз; обязательная; запрашивает {0}; по умолчанию - {0}Аргумент опции {name} должен быть типа {argtype}, но не {val!r}: {ex!r}Ошибка: {0}Ожидая как минимум {0} позиционный аргумент, получено {1}Ожидая как минимум {0} позиционных аргумента, получено {1}Ожидая как минимум {0} позиционных аргументов, получено {1}Ожидая как максимум {0} позиционный аргумент, получено {1}Ожидая как максимум {0} позиционных аргумента, получено {1}Ожидая как максимум {0} позиционных аргументов, получено {1}При передаче {0}, нельзя указать {1}При передаче {0}, необходимо также указать {1}Мета-опцииПодкоманда не заданаНе в промежутке [{0:d}..{1:d}]Печатает версию этой программы и выходитПечатает это сообщение и выходитПодкоманда({self.name}, {self.subapplication})Опция {0} уже была переданаОпция {0} уже была передана (эквивалентна {1})Опция {0} - это булев флагОпция {0} обязательнаОпция {0} должна быть последовательностью (перечислением)Для опции {0} необходим аргументОпция {name} уже определена и не может быть переопределенаОпцииНеизестная подкоманда '{0}'Неизестная опция {0}Использование:ЗНАЧЕНИЕФункция main() не реализованавызовите '{parent} {sub} --help' для более полной справки{0} уже была передана{0} - это не папка{0} - это не файлplumbum-1.8.3/plumbum/colorlib/__init__.py0000644000000000000000000000227314613634536015550 0ustar00"""\ The ``ansicolor`` object provides ``bg`` and ``fg`` to access colors, and attributes like bold and underlined text. It also provides ``reset`` to recover the normal font. """ import sys from .factories import StyleFactory from .styles import ANSIStyle, ColorNotFound, HTMLStyle, Style __all__ = ( "ANSIStyle", "ColorNotFound", "HTMLStyle", "Style", "StyleFactory", "ansicolors", "htmlcolors", "load_ipython_extension", "main", ) ansicolors = StyleFactory(ANSIStyle) htmlcolors = StyleFactory(HTMLStyle) def load_ipython_extension(ipython): # pragma: no cover try: from ._ipython_ext import OutputMagics # pylint:disable=import-outside-toplevel except ImportError: print("IPython required for the IPython extension to be loaded.") # noqa: T201 raise ipython.push({"colors": htmlcolors}) ipython.register_magics(OutputMagics) def main(): # pragma: no cover """Color changing script entry. Call using python3 -m plumbum.colors, will reset if no arguments given.""" color = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "" ansicolors.use_color = True ansicolors.get_colors_from_string(color).now() plumbum-1.8.3/plumbum/colorlib/__main__.py0000644000000000000000000000024414613634536015525 0ustar00""" This is provided as a quick way to recover your terminal. Simply run ``python3 -m plumbum.colorlib`` to recover terminal color. """ from . import main main() plumbum-1.8.3/plumbum/colorlib/_ipython_ext.py0000644000000000000000000000172314613634536016521 0ustar00import sys 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 x[:8] == "display_"] @magics_class class OutputMagics(Magics): # pragma: no cover @needs_local_scope @cell_magic def to(self, line, cell, local_ns=None): choice = line.strip() assert choice in valid_choices, "Valid choices for '%%to' are: " + str( valid_choices ) display_fn = getattr(IPython.display, "display_" + choice) # Captures stdout and renders it in the notebook with StringIO() as out: old_out = sys.stdout try: sys.stdout = out exec(cell, self.shell.user_ns, local_ns) # pylint: disable=exec-used out.seek(0) display_fn(out.getvalue(), raw=True) finally: sys.stdout = old_out plumbum-1.8.3/plumbum/colorlib/factories.py0000644000000000000000000001541614613634536015773 0ustar00""" Color-related factories. They produce Styles. """ import functools import operator import sys from typing import Any from .names import color_names, default_styles from .styles import ColorNotFound __all__ = ["ColorFactory", "StyleFactory"] class ColorFactory: """This creates color names given fg = True/False. It usually will be called as part of a StyleFactory.""" def __init__(self, fg, style): self._fg = fg self._style = style self.reset = style.from_color(style.color_class(fg=fg)) # Adding the color name shortcuts for foreground colors for item in color_names[:16]: setattr( self, item, style.from_color(style.color_class.from_simple(item, fg=fg)) ) def __getattr__(self, item): """Full color names work, but do not populate __dir__.""" try: return self._style.from_color(self._style.color_class(item, fg=self._fg)) except ColorNotFound: raise AttributeError(item) from None def full(self, name): """Gets the style for a color, using standard name procedure: either full color name, html code, or number.""" return self._style.from_color( self._style.color_class.from_full(name, fg=self._fg) ) def simple(self, name): """Return the extended color scheme color for a value or name.""" return self._style.from_color( self._style.color_class.from_simple(name, fg=self._fg) ) def rgb(self, r, g=None, b=None): """Return the extended color scheme color for a value.""" if g is None and b is None: return self.hex(r) return self._style.from_color(self._style.color_class(r, g, b, fg=self._fg)) def hex(self, hexcode): """Return the extended color scheme color for a value.""" return self._style.from_color( self._style.color_class.from_hex(hexcode, fg=self._fg) ) def ansi(self, ansiseq): """Make a style from an ansi text sequence""" return self._style.from_ansi(ansiseq) def __getitem__(self, val): """\ Shortcut to provide way to access colors numerically or by slice. If end <= 16, will stay to simple ANSI version.""" if isinstance(val, slice): (start, stop, stride) = val.indices(256) if stop <= 16: return [self.simple(v) for v in range(start, stop, stride)] return [self.full(v) for v in range(start, stop, stride)] if isinstance(val, tuple): return self.rgb(*val) try: return self.full(val) except ColorNotFound: return self.hex(val) def __call__(self, val_or_r=None, g=None, b=None): """Shortcut to provide way to access colors.""" if val_or_r is None or (isinstance(val_or_r, str) and val_or_r == ""): return self._style() if isinstance(val_or_r, self._style): return self._style(val_or_r) if isinstance(val_or_r, str) and "\033" in val_or_r: return self.ansi(val_or_r) return self._style.from_color( self._style.color_class(val_or_r, g, b, fg=self._fg) ) def __iter__(self): """Iterates through all colors in extended colorset.""" return (self.full(i) for i in range(256)) def __invert__(self): """Allows clearing a color with ~""" return self.reset def __enter__(self): """This will reset the color on leaving the with statement.""" return self def __exit__(self, _type: Any, _value: Any, _traceback: Any) -> None: """This resets a FG/BG color or all styles, due to different definition of RESET for the factories.""" self.reset.now() def __repr__(self): """Simple representation of the class by name.""" return f"<{self.__class__.__name__}>" class StyleFactory(ColorFactory): """Factory for styles. Holds font styles, FG and BG objects representing colors, and imitates the FG ColorFactory to a large degree.""" def __init__(self, style): super().__init__(True, style) self.fg = ColorFactory(True, style) self.bg = ColorFactory(False, style) self.do_nothing = style() self.reset = style(reset=True) for item in style.attribute_names: setattr(self, item, style(attributes={item: True})) self.load_stylesheet(default_styles) @property def use_color(self): """Shortcut for setting color usage on Style""" return self._style.use_color @use_color.setter def use_color(self, val): self._style.use_color = val def from_ansi(self, ansi_sequence): """Calling this is a shortcut for creating a style from an ANSI sequence.""" return self._style.from_ansi(ansi_sequence) @property def stdout(self): """This is a shortcut for getting stdout from a class without an instance.""" return self._style._stdout if self._style._stdout is not None else sys.stdout @stdout.setter def stdout(self, newout): self._style._stdout = newout def get_colors_from_string(self, color=""): """ Sets color based on string, use `.` or space for separator, and numbers, fg/bg, htmlcodes, etc all accepted (as strings). """ names = color.replace(".", " ").split() prev = self styleslist = [] for name in names: try: prev = getattr(prev, name) except AttributeError: try: prev = prev(int(name)) except (ColorNotFound, ValueError): prev = prev(name) if isinstance(prev, self._style): styleslist.append(prev) prev = self if styleslist: prev = functools.reduce(operator.and_, styleslist) return prev if isinstance(prev, self._style) else prev.reset def filter(self, colored_string): """Filters out colors in a string, returning only the name.""" if isinstance(colored_string, self._style): return colored_string return self._style.string_filter_ansi(colored_string) def contains_colors(self, colored_string): """Checks to see if a string contains colors.""" return self._style.string_contains_colors(colored_string) def extract(self, colored_string): """Gets colors from an ansi string, returns those colors""" return self._style.from_ansi(colored_string, True) def load_stylesheet(self, stylesheet=None): if stylesheet is None: stylesheet = default_styles for item in stylesheet: setattr(self, item, self.get_colors_from_string(stylesheet[item])) plumbum-1.8.3/plumbum/colorlib/names.py0000644000000000000000000002010514613634536015106 0ustar00""" Names for the standard and extended color set. Extended set is similar to `vim wiki `_, `colored `_, etc. Colors based on `wikipedia `_. You can access the index of the colors with names.index(name). You can access the rgb values with ``r=int(html[n][1:3],16)``, etc. """ from typing import Tuple color_names = """\ black red green yellow blue magenta cyan light_gray dark_gray light_red light_green light_yellow light_blue light_magenta light_cyan white grey_0 navy_blue dark_blue blue_3 blue_3a blue_1 dark_green deep_sky_blue_4 deep_sky_blue_4a deep_sky_blue_4b dodger_blue_3 dodger_blue_2 green_4 spring_green_4 turquoise_4 deep_sky_blue_3 deep_sky_blue_3a dodger_blue_1 green_3 spring_green_3 dark_cyan light_sea_green deep_sky_blue_2 deep_sky_blue_1 green_3a spring_green_3a spring_green_2 cyan_3 dark_turquoise turquoise_2 green_1 spring_green_2a spring_green_1 medium_spring_green cyan_2 cyan_1 dark_red deep_pink_4 purple_4 purple_4a purple_3 blue_violet orange_4 grey_37 medium_purple_4 slate_blue_3 slate_blue_3a royal_blue_1 chartreuse_4 dark_sea_green_4 pale_turquoise_4 steel_blue steel_blue_3 cornflower_blue chartreuse_3 dark_sea_green_4a cadet_blue cadet_blue_a sky_blue_3 steel_blue_1 chartreuse_3a pale_green_3 sea_green_3 aquamarine_3 medium_turquoise steel_blue_1a chartreuse_2a sea_green_2 sea_green_1 sea_green_1a aquamarine_1 dark_slate_gray_2 dark_red_a deep_pink_4a dark_magenta dark_magenta_a dark_violet purple orange_4a light_pink_4 plum_4 medium_purple_3 medium_purple_3a slate_blue_1 yellow_4 wheat_4 grey_53 light_slate_grey medium_purple light_slate_blue yellow_4_a dark_olive_green_3 dark_sea_green light_sky_blue_3 light_sky_blue_3a sky_blue_2 chartreuse_2 dark_olive_green_3a pale_green_3a dark_sea_green_3 dark_slate_gray_3 sky_blue_1 chartreuse_1 light_green_a light_green_b pale_green_1 aquamarine_1a dark_slate_gray_1 red_3 deep_pink_4b medium_violet_red magenta_3 dark_violet_a purple_a dark_orange_3 indian_red hot_pink_3 medium_orchid_3 medium_orchid medium_purple_2 dark_goldenrod light_salmon_3 rosy_brown grey_63 medium_purple_2a medium_purple_1 gold_3 dark_khaki navajo_white_3 grey_69 light_steel_blue_3 light_steel_blue yellow_3 dark_olive_green_3b dark_sea_green_3a dark_sea_green_2 light_cyan_3 light_sky_blue_1 green_yellow dark_olive_green_2 pale_green_1a dark_sea_green_2a dark_sea_green_1 pale_turquoise_1 red_3a deep_pink_3 deep_pink_3a magenta_3a magenta_3b magenta_2 dark_orange_3a indian_red_a hot_pink_3a hot_pink_2 orchid medium_orchid_1 orange_3 light_salmon_3a light_pink_3 pink_3 plum_3 violet gold_3a light_goldenrod_3 tan misty_rose_3 thistle_3 plum_2 yellow_3a khaki_3 light_goldenrod_2 light_yellow_3 grey_84 light_steel_blue_1 yellow_2 dark_olive_green_1 dark_olive_green_1a dark_sea_green_1a honeydew_2 light_cyan_1 red_1 deep_pink_2 deep_pink_1 deep_pink_1a magenta_2a magenta_1 orange_red_1 indian_red_1 indian_red_1a hot_pink hot_pink_a medium_orchid_1a dark_orange salmon_1 light_coral pale_violet_red_1 orchid_2 orchid_1 orange_1 sandy_brown light_salmon_1 light_pink_1 pink_1 plum_1 gold_1 light_goldenrod_2a light_goldenrod_2b navajo_white_1 misty_rose_1 thistle_1 yellow_1 light_goldenrod_1 khaki_1 wheat_1 cornsilk_1 grey_10_0 grey_3 grey_7 grey_11 grey_15 grey_19 grey_23 grey_27 grey_30 grey_35 grey_39 grey_42 grey_46 grey_50 grey_54 grey_58 grey_62 grey_66 grey_70 grey_74 grey_78 grey_82 grey_85 grey_89 grey_93""".split() EMPTY_SLICE = slice(None, None, None) _greys = ( 3.4, 7.4, 11, 15, 19, 23, 26.7, 30.49, 34.6, 38.6, 42.4, 46.4, 50, 54, 58, 62, 66, 69.8, 73.8, 77.7, 81.6, 85.3, 89.3, 93, ) _grey_vals = [int(x / 100.0 * 16 * 16) for x in _greys] _grey_html = ["#" + format(x, "02x") * 3 for x in _grey_vals] _normals = [int(x, 16) for x in "0 5f 87 af d7 ff".split()] _normal_html = [ "#" + format(_normals[n // 36], "02x") + format(_normals[n // 6 % 6], "02x") + format(_normals[n % 6], "02x") for n in range(16 - 16, 232 - 16) ] _base_pattern = [(n // 4, n // 2 % 2, n % 2) for n in range(8)] _base_html = ( [f"#{x[2] * 192:02x}{x[1] * 192:02x}{x[0] * 192:02x}" for x in _base_pattern] + ["#808080"] + [f"#{x[2] * 255:02x}{x[1] * 255:02x}{x[0] * 255:02x}" for x in _base_pattern][1:] ) color_html = _base_html + _normal_html + _grey_html 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 = { "bold": 1, "dim": 2, "italics": 3, "underline": 4, "reverse": 7, "hidden": 8, "strikeout": 9, } # Stylesheet 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 class FindNearest: """This is a class for finding the nearest color given rgb values. Different find methods are available.""" def __init__(self, r: int, g: int, b: int) -> None: self.r = r self.b = b self.g = g def only_basic(self): """This will only return the first 8 colors! Breaks the colorspace into cubes, returns color""" midlevel = 0x40 # Since bright is not included # The colors are organised so that it is a # 3D cube, black at 0,0,0, white at 1,1,1 # Compressed to linear_integers r,g,b # [[[0,1],[2,3]],[[4,5],[6,7]]] # r*1 + g*2 + b*4 return ( (self.r >= midlevel) * 1 + (self.g >= midlevel) * 2 + (self.b >= midlevel) * 4 ) def all_slow(self, color_slice: slice = EMPTY_SLICE) -> int: """This is a slow way to find the nearest color.""" distances = [ self._distance_to_color(color) for color in color_html[color_slice] ] return min(range(len(distances)), key=distances.__getitem__) def _distance_to_color(self, color: str) -> int: """This computes the distance to a color, should be minimized.""" rgb = (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) return (self.r - rgb[0]) ** 2 + (self.g - rgb[1]) ** 2 + (self.b - rgb[2]) ** 2 def _distance_to_color_number(self, n: int) -> int: color = color_html[n] return self._distance_to_color(color) def only_colorblock(self) -> int: """This finds the nearest color based on block system, only works for 17-232 color values.""" rint = min( range(len(_normals)), key=[abs(x - self.r) for x in _normals].__getitem__ ) bint = min( range(len(_normals)), key=[abs(x - self.b) for x in _normals].__getitem__ ) gint = min( range(len(_normals)), key=[abs(x - self.g) for x in _normals].__getitem__ ) return 16 + 36 * rint + 6 * gint + bint def only_simple(self) -> int: """Finds the simple color-block color.""" return self.all_slow(slice(0, 16, None)) def only_grey(self) -> int: """Finds the greyscale color.""" rawval = (self.r + self.b + self.g) / 3 n = min( range(len(_grey_vals)), key=[abs(x - rawval) for x in _grey_vals].__getitem__, ) return n + 232 def all_fast(self) -> int: """Runs roughly 8 times faster than the slow version.""" colors = [self.only_simple(), self.only_colorblock(), self.only_grey()] distances = [self._distance_to_color_number(n) for n in colors] return colors[min(range(len(distances)), key=distances.__getitem__)] def from_html(color: str) -> Tuple[int, int, int]: """Convert html hex code to rgb.""" if len(color) != 7 or color[0] != "#": raise ValueError("Invalid length of html code") return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) def to_html(r, g, b): """Convert rgb to html hex code.""" return f"#{r:02x}{g:02x}{b:02x}" plumbum-1.8.3/plumbum/colorlib/styles.py0000644000000000000000000006072614613634536015343 0ustar00""" This file provides two classes, `Color` and `Style`. ``Color`` is rarely used directly, but merely provides the workhorse for finding and manipulating colors. With the ``Style`` class, any color can be directly called or given to a with statement. """ import contextlib import os import platform import re import sys from abc import ABCMeta, abstractmethod from copy import copy from typing import IO, Dict, Optional, Union from .names import ( FindNearest, attributes_ansi, color_codes_simple, color_html, color_names, from_html, ) __all__ = [ "Color", "Style", "ANSIStyle", "HTMLStyle", "ColorNotFound", "AttributeNotFound", ] _lower_camel_names = [n.replace("_", "") for n in color_names] def get_color_repr(): """Gets best colors for current system.""" if "NO_COLOR" in os.environ: return 0 if os.environ.get("FORCE_COLOR", "") in {"0", "1", "2", "3", "4"}: return int(os.environ["FORCE_COLOR"]) if not sys.stdout.isatty(): return 0 term = os.environ.get("TERM", "") # Some terminals set TERM=xterm for compatibility if term.endswith("256color") or term == "xterm": return 3 if platform.system() == "Darwin" else 4 if term.endswith("16color"): return 2 if term == "screen": return 1 if os.name == "nt": return 0 return 3 class ColorNotFound(Exception): """Thrown when a color is not valid for a particular method.""" class AttributeNotFound(Exception): """Similar to color not found, only for attributes.""" class ResetNotSupported(Exception): """An exception indicating that Reset is not available for this Style.""" class Color: """\ Loaded with ``(r, g, b, fg)`` or ``(color, fg=fg)``. The second signature is a short cut and will try full and hex loading. This class stores the idea of a color, rather than a specific implementation. It provides as many different tools for representations as possible, and can be subclassed to add more representations, though that should not be needed for most situations. ``.from_`` class methods provide quick ways to create colors given different representations. You will not usually interact with this class. Possible colors:: reset = Color() # The reset color by default background_reset = Color(fg=False) # Can be a background color blue = Color(0,0,255) # Red, Green, Blue green = Color.from_full("green") # Case insensitive name, from large colorset red = Color.from_full(1) # Color number white = Color.from_html("#FFFFFF") # HTML supported yellow = Color.from_simple("red") # Simple colorset The attributes are: .. data:: reset True it this is a reset color (following attributes don't matter if True) .. data:: rgb The red/green/blue tuple for this color .. data:: simple If true will stay to 16 color mode. .. data:: number The color number given the mode, closest to rgb if not rgb not exact, gives position of closest name. .. data:: fg This is a foreground color if True. Background color if False. """ __slots__ = ("fg", "isreset", "rgb", "number", "representation", "exact") def __init__(self, r_or_color=None, g=None, b=None, fg=True): """This works from color values, or tries to load non-simple ones.""" if isinstance(r_or_color, type(self)): for item in ("fg", "isreset", "rgb", "number", "representation", "exact"): setattr(self, item, getattr(r_or_color, item)) return self.fg = fg self.isreset = True # Starts as reset color self.rgb = (0, 0, 0) self.number = None "Number of the original color, or closest color" self.representation = 4 "0 for off, 1 for 8 colors, 2 for 16 colors, 3 for 256 colors, 4 for true color" self.exact = True "This is false if the named color does not match the real color" if None in (g, b): if not r_or_color: return try: self._from_simple(r_or_color) except ColorNotFound: try: self._from_full(r_or_color) except ColorNotFound: self._from_hex(r_or_color) elif None not in (r_or_color, g, b): self.rgb = (r_or_color, g, b) self._init_number() else: raise ColorNotFound("Invalid parameters for a color!") def _init_number(self): """Should always be called after filling in r, g, b, and representation. Color will not be a reset color anymore.""" if self.representation in (0, 1): number = FindNearest(*self.rgb).only_basic() elif self.representation == 2: number = FindNearest(*self.rgb).only_simple() elif self.representation in (3, 4): number = FindNearest(*self.rgb).all_fast() if self.number is None: self.number = number self.isreset = False self.exact = self.rgb == from_html(color_html[self.number]) if not self.exact: self.number = number @classmethod def from_simple(cls, color, fg=True): """Creates a color from simple name or color number""" self = cls(fg=fg) self._from_simple(color) return self def _from_simple(self, color): with contextlib.suppress(AttributeError): color = color.lower() color = color.replace(" ", "") color = color.replace("_", "") if color == "reset": return if color in _lower_camel_names[:16]: self.number = _lower_camel_names.index(color) self.rgb = from_html(color_html[self.number]) elif isinstance(color, int) and 0 <= color < 16: self.number = color self.rgb = from_html(color_html[color]) else: raise ColorNotFound("Did not find color: " + repr(color)) self.representation = 2 self._init_number() @classmethod def from_full(cls, color, fg=True): """Creates a color from full name or color number""" self = cls(fg=fg) self._from_full(color) return self def _from_full(self, color): with contextlib.suppress(AttributeError): color = color.lower() color = color.replace(" ", "") color = color.replace("_", "") if color == "reset": return if color in _lower_camel_names: self.number = _lower_camel_names.index(color) self.rgb = from_html(color_html[self.number]) elif isinstance(color, int) and 0 <= color <= 255: self.number = color self.rgb = from_html(color_html[color]) else: raise ColorNotFound("Did not find color: " + repr(color)) self.representation = 3 self._init_number() @classmethod def from_hex(cls, color, fg=True): """Converts #123456 values to colors.""" self = cls(fg=fg) self._from_hex(color) return self def _from_hex(self, color): try: self.rgb = from_html(color) except (TypeError, ValueError): raise ColorNotFound("Did not find htmlcode: " + repr(color)) from None self.representation = 4 self._init_number() @property def name(self): """The (closest) name of the current color""" return "reset" if self.isreset else color_names[self.number] @property def name_camelcase(self): """The camelcase name of the color""" return self.name.replace("_", " ").title().replace(" ", "") def __repr__(self): """This class has a smart representation that shows name and color (if not unique).""" name = ["Deactivated:", " Basic:", "", " Full:", " True:"][self.representation] name += "" if self.fg else " Background" name += " " + self.name_camelcase name += "" if self.exact else " " + self.hex_code return name[1:] def __eq__(self, other): """Reset colors are equal, otherwise rgb have to match.""" return other.isreset if self.isreset else self.rgb == other.rgb @property def ansi_sequence(self): """This is the ansi sequence as a string, ready to use.""" return "\033[" + ";".join(map(str, self.ansi_codes)) + "m" @property def ansi_codes(self): """This is the full ANSI code, can be reset, simple, 256, or full color.""" ansi_addition = 30 if self.fg else 40 if self.isreset: return (ansi_addition + 9,) if self.representation < 3: return (color_codes_simple[self.number] + ansi_addition,) if self.representation == 3: return (ansi_addition + 8, 5, self.number) return (ansi_addition + 8, 2, self.rgb[0], self.rgb[1], self.rgb[2]) @property def hex_code(self): """This is the hex code of the current color, html style notation.""" return ( "#000000" if self.isreset else f"#{self.rgb[0]:02X}{self.rgb[1]:02X}{self.rgb[2]:02X}" ) def __str__(self): """This just prints it's simple name""" return self.name def to_representation(self, val): """Converts a color to any representation""" other = copy(self) other.representation = val if self.isreset: return other other.number = None other._init_number() return other def limit_representation(self, val): """Only converts if val is lower than representation""" return self if self.representation <= val else self.to_representation(val) class Style(metaclass=ABCMeta): """This class allows the color changes to be called directly to write them to stdout, ``[]`` calls to wrap colors (or the ``.wrap`` method) and can be called in a with statement. """ __slots__ = ("attributes", "fg", "bg", "isreset", "__weakref__") color_class = Color """The class of color to use. Never hardcode ``Color`` call when writing a Style method.""" attribute_names: Union[Dict[str, str], Dict[str, int]] _stdout: Optional[IO] = None end = "\n" """The endline character. Override if needed in subclasses.""" ANSI_REG = re.compile("\033\\[([\\d;]+)m") """The regular expression that finds ansi codes in a string.""" @property def stdout(self): """\ This property will allow custom, class level control of stdout. It will use current sys.stdout if set to None (default). Unfortunately, it only works on an instance.. """ # Import sys repeated here to make calling this stable in atexit function import sys # pylint: disable=reimported, redefined-outer-name, import-outside-toplevel return ( self.__class__._stdout if self.__class__._stdout is not None else sys.stdout ) @stdout.setter def stdout(self, newout): self.__class__._stdout = newout def __init__(self, attributes=None, fgcolor=None, bgcolor=None, reset=False): """This is usually initialized from a factory.""" if isinstance(attributes, type(self)): for item in ("attributes", "fg", "bg", "isreset"): setattr(self, item, copy(getattr(attributes, item))) return self.attributes = attributes if attributes is not None else {} self.fg = fgcolor self.bg = bgcolor self.isreset = reset invalid_attributes = set(self.attributes) - set(self.attribute_names) if len(invalid_attributes) > 0: raise AttributeNotFound( "Attribute(s) not valid: " + ", ".join(invalid_attributes) ) @classmethod def from_color(cls, color): 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 attributes present""" other = self.__class__() # Opposite of reset is reset if self.isreset: other.isreset = True return other # Flip all attributes for attribute in self.attributes: other.attributes[attribute] = not self.attributes[attribute] # Reset only if color present if self.fg: other.fg = self.fg.__class__() if self.bg: other.bg = self.bg.__class__() return other @property def reset(self): """Shortcut to access reset as a property.""" return self.invert() def __copy__(self): """Copy is supported, will make dictionary and colors unique.""" result = self.__class__() result.isreset = self.isreset result.fg = copy(self.fg) result.bg = copy(self.bg) result.attributes = copy(self.attributes) return result def __invert__(self): """This allows ~color.""" return self.invert() def __add__(self, other): """Adding two matching Styles results in a new style with the combination of both. Adding with a string results in the string concatenation of a style. Addition is non-commutative, with the rightmost Style property being taken if both have the same property. (Not safe)""" if type(self) == type(other): result = copy(other) result.isreset = self.isreset or other.isreset for attribute in self.attributes: if attribute not in result.attributes: result.attributes[attribute] = self.attributes[attribute] if not result.fg: result.fg = self.fg if not result.bg: result.bg = self.bg return result return other.__class__(self) + other def __radd__(self, other): """This only gets called if the string is on the left side. (Not safe)""" return other + other.__class__(self) def wrap(self, wrap_this): """Wrap a string in this style and its inverse.""" return self + wrap_this + ~self def __and__(self, other): """This class supports ``color & color2`` syntax, and ``color & "String" syntax too.``""" if type(self) == type(other): return self + other return self.wrap(other) def __rand__(self, other): """This class supports ``"String:" & color`` syntax.""" return self.wrap(other) def __ror__(self, other): """Support for "String" | color syntax""" return self.wrap(other) def __or__(self, other): """This class supports ``color | color2`` syntax. It also supports ``"color | "String"`` syntax too.""" return self.__and__(other) def __call__(self): """\ This is a shortcut to print color immediately to the stdout. (Not safe) """ self.now() def now(self): """Immediately writes color to stdout. (Not safe)""" self.stdout.write(str(self)) def print(self, *printables, **kargs): """\ This acts like print; will print that argument to stdout wrapped in Style with the same syntax as the print function in 3.4.""" end = kargs.get("end", self.end) sep = kargs.get("sep", " ") file = kargs.get("file", self.stdout) flush = kargs.get("flush", False) file.write(self.wrap(sep.join(map(str, printables))) + end) if flush: file.flush() print_ = print """DEPRECATED: Shortcut from classic Python 2""" def __getitem__(self, wrapped): """The [] syntax is supported for wrapping""" return self.wrap(wrapped) def __enter__(self): """Context manager support""" self.stdout.write(str(self)) self.stdout.flush() def __exit__(self, _type, _value, _traceback): """Runs even if exception occurred, does not catch it.""" self.stdout.write(str(~self)) self.stdout.flush() return False @property def ansi_codes(self): """Generates the full ANSI code sequence for a Style""" if self.isreset: return [0] codes = [] for attribute in self.attributes: if self.attributes[attribute]: codes.append(attributes_ansi[attribute]) else: # Fixing bold inverse being 22 instead of 21 on some terminals: codes.append( attributes_ansi[attribute] + 20 if attributes_ansi[attribute] != 1 else 22 ) if self.fg: codes.extend(self.fg.ansi_codes) if self.bg: self.bg.fg = False codes.extend(self.bg.ansi_codes) return codes @property def ansi_sequence(self): """This is the string ANSI sequence.""" codes = ";".join(str(c) for c in self.ansi_codes) return f"\033[{codes}m" if codes else "" def __repr__(self): name = self.__class__.__name__ attributes = ", ".join(a for a in self.attributes if self.attributes[a]) neg_attributes = ", ".join( f"-{a}" for a in self.attributes if not self.attributes[a] ) colors = ", ".join(repr(c) for c in (self.fg, self.bg) if c) string = ( "; ".join(s for s in (attributes, neg_attributes, colors) if s) or "empty" ) if self.isreset: string = "reset" return f"<{name}: {string}>" 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 return ( self.attributes == other.attributes and self.fg == other.fg and self.bg == other.bg ) return str(self) == other @abstractmethod def __str__(self): """Base Style does not implement a __str__ representation. This is the one required method of a subclass.""" @classmethod def from_ansi(cls, ansi_string, filter_resets=False): """This generated a style from an ansi string. Will ignore resets if filter_resets is True.""" result = cls() res = cls.ANSI_REG.search(ansi_string) for group in res.groups(): sequence = map(int, group.split(";")) result.add_ansi(sequence, filter_resets) return result def add_ansi(self, sequence, filter_resets=False): """Adds a sequence of ansi numbers to the class. Will ignore resets if filter_resets is True.""" values = iter(sequence) try: while True: value = next(values) if value in {38, 48}: fg = value == 38 value = next(values) if value == 5: value = next(values) if fg: self.fg = self.color_class.from_full(value) else: self.bg = self.color_class.from_full(value, fg=False) elif value == 2: r = next(values) g = next(values) b = next(values) if fg: self.fg = self.color_class(r, g, b) else: self.bg = self.color_class(r, g, b, fg=False) else: raise ColorNotFound("the value 5 or 2 should follow a 38 or 48") elif value == 0: if filter_resets is False: self.isreset = True elif value in attributes_ansi.values(): for name, att_value in attributes_ansi.items(): if value == att_value: self.attributes[name] = True elif value in (20 + n for n in attributes_ansi.values()): if filter_resets is False: for name, att_value in attributes_ansi.items(): if value == att_value + 20: self.attributes[name] = False elif 30 <= value <= 37: self.fg = self.color_class.from_simple(value - 30) elif 40 <= value <= 47: self.bg = self.color_class.from_simple(value - 40, fg=False) elif 90 <= value <= 97: self.fg = self.color_class.from_simple(value - 90 + 8) elif 100 <= value <= 107: self.bg = self.color_class.from_simple(value - 100 + 8, fg=False) elif value == 39: if filter_resets is False: self.fg = self.color_class() elif value == 49: if filter_resets is False: self.bg = self.color_class(fg=False) else: raise ColorNotFound(f"The code {value} is not recognised") except StopIteration: return @classmethod def string_filter_ansi(cls, colored_string): """Filters out colors in a string, returning only the name.""" return cls.ANSI_REG.sub("", colored_string) @classmethod def string_contains_colors(cls, colored_string): """Checks to see if a string contains colors.""" return len(cls.ANSI_REG.findall(colored_string)) > 0 def to_representation(self, rep): """This converts both colors to a specific representation""" other = copy(self) if other.fg: other.fg = other.fg.to_representation(rep) if other.bg: other.bg = other.bg.to_representation(rep) return other def limit_representation(self, rep): """This only converts if true representation is higher""" if rep is True or rep is False: return self other = copy(self) if other.fg: other.fg = other.fg.limit_representation(rep) if other.bg: other.bg = other.bg.limit_representation(rep) return other @property def basic(self): """The color in the 8 color representation.""" return self.to_representation(1) @property def simple(self): """The color in the 16 color representation.""" return self.to_representation(2) @property def full(self): """The color in the 256 color representation.""" return self.to_representation(3) @property def true(self): """The color in the true color representation.""" return self.to_representation(4) class ANSIStyle(Style): """This is a subclass for ANSI styles. Use it to get color on sys.stdout tty terminals on posix systems. Set ``use_color = True/False`` if you want to control color for anything using this Style.""" __slots__ = () use_color = get_color_repr() attribute_names = attributes_ansi def __str__(self): return ( self.limit_representation(self.use_color).ansi_sequence if self.use_color else "" ) class HTMLStyle(Style): """This was meant to be a demo of subclassing Style, but actually can be a handy way to quickly color html text.""" __slots__ = () 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 = "
\n" def __str__(self): if self.isreset: raise ResetNotSupported("HTML does not support global resets!") result = "" if self.bg and not self.bg.isreset: result += f'' if self.fg and not self.fg.isreset: result += f'' for attr in sorted(self.attributes): if self.attributes[attr]: result += "<" + self.attribute_names[attr] + ">" for attr in sorted(self.attributes, reverse=True): if not self.attributes[attr]: result += "" if self.fg and self.fg.isreset: result += "" if self.bg and self.bg.isreset: result += "" return result plumbum-1.8.3/plumbum/commands/__init__.py0000644000000000000000000000140514613634536015540 0ustar00from plumbum.commands.base import ( ERROUT, BaseCommand, ConcreteCommand, shquote, shquote_list, ) from plumbum.commands.modifiers import ( BG, FG, NOHUP, RETCODE, TEE, TF, ExecutionModifier, Future, ) from plumbum.commands.processes import ( CommandNotFound, ProcessExecutionError, ProcessLineTimedOut, ProcessTimedOut, run_proc, ) __all__ = ( "BaseCommand", "ConcreteCommand", "shquote", "shquote_list", "ERROUT", "BG", "FG", "NOHUP", "RETCODE", "TEE", "TF", "ExecutionModifier", "Future", "CommandNotFound", "ProcessExecutionError", "ProcessLineTimedOut", "ProcessTimedOut", "run_proc", ) def __dir__(): return __all__ plumbum-1.8.3/plumbum/commands/base.py0000644000000000000000000004601014613634536014714 0ustar00import contextlib import functools import shlex import subprocess from subprocess import PIPE, Popen from tempfile import TemporaryFile from types import MethodType from typing import ClassVar import plumbum.commands.modifiers from plumbum.commands.processes import iter_lines, run_proc __all__ = ( "iter_lines", "run_proc", "shquote", "shquote_list", "RedirectionError", "BaseCommand", "Pipeline", "BaseRedirection", "BoundCommand", "BoundEnvCommand", "ConcreteCommand", "ERROUT", "StdinRedirection", "StdoutRedirection", "StderrRedirection", "AppendingStdoutRedirection", "StdinDataRedirection", ) class RedirectionError(Exception): """Raised when an attempt is made to redirect an process' standard handle, which was already redirected to/from a file""" # =================================================================================================== # Utilities # =================================================================================================== # modified from the stdlib pipes module for windows _safechars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@%_-+=:,./" _funnychars = '"`$\\' def shquote(text): """Quotes the given text with shell escaping (assumes as syntax similar to ``sh``)""" text = str(text) return shlex.quote(text) def shquote_list(seq): return [shquote(item) for item in seq] # =================================================================================================== # Commands # =================================================================================================== class BaseCommand: """Base of all command objects""" __slots__ = ("cwd", "env", "custom_encoding", "__weakref__") def __str__(self): return " ".join(self.formulate()) def __or__(self, other): """Creates a pipe with the other command""" return Pipeline(self, other) def __gt__(self, file): """Redirects the process' stdout to the given file""" return StdoutRedirection(self, file) def __rshift__(self, file): """Redirects the process' stdout to the given file (appending)""" return AppendingStdoutRedirection(self, file) def __ge__(self, file): """Redirects the process' stderr to the given file""" return StderrRedirection(self, file) def __lt__(self, file): """Redirects the given file into the process' stdin""" return StdinRedirection(self, file) def __lshift__(self, data): """Redirects the given data into the process' stdin""" return StdinDataRedirection(self, data) def __getitem__(self, args): """Creates a bound-command with the given arguments. Shortcut for bound_command.""" if not isinstance(args, (tuple, list)): args = [ args, ] return self.bound_command(*args) def bound_command(self, *args): """Creates a bound-command with the given arguments""" if not args: return self if isinstance(self, BoundCommand): return BoundCommand(self.cmd, self.args + list(args)) return BoundCommand(self, args) def __call__(self, *args, **kwargs): """A shortcut for `run(args)`, returning only the process' stdout""" return self.run(args, **kwargs)[1] def _get_encoding(self): raise NotImplementedError() def with_env(self, **env): """Returns a BoundEnvCommand with the given environment variables""" if not env: return self return BoundEnvCommand(self, env=env) def with_cwd(self, path): """ Returns a BoundEnvCommand with the specified working directory. This overrides a cwd specified in a wrapping `machine.cwd()` context manager. """ if not path: return self return BoundEnvCommand(self, cwd=path) setenv = with_env @property def machine(self): raise NotImplementedError() def formulate(self, level=0, args=()): """Formulates the command into a command-line, i.e., a list of shell-quoted strings that can be executed by ``Popen`` or shells. :param level: The nesting level of the formulation; it dictates how much shell-quoting (if any) should be performed :param args: The arguments passed to this command (a tuple) :returns: A list of strings """ raise NotImplementedError() def popen(self, args=(), **kwargs): """Spawns the given command, returning a ``Popen``-like object. .. note:: When processes run in the **background** (either via ``popen`` or :class:`& BG `), their stdout/stderr pipes might fill up, causing them to hang. If you know a process produces output, be sure to consume it every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ :param args: Any arguments to be passed to the process (a tuple) :param kwargs: Any keyword-arguments to be passed to the ``Popen`` constructor :returns: A ``Popen``-like object """ raise NotImplementedError() def nohup(self, cwd=".", stdout="nohup.out", stderr=None, append=True): """Runs a command detached.""" return self.machine.daemonic_popen(self, cwd, stdout, stderr, append) @contextlib.contextmanager def bgrun(self, args=(), **kwargs): """Runs the given command as a context manager, allowing you to create a `pipeline `_ (not in the UNIX sense) of programs, parallelizing their work. In other words, instead of running programs one after the other, you can start all of them at the same time and wait for them to finish. For a more thorough review, see `Lightweight Asynchronism `_. Example:: from plumbum.cmd import mkfs with mkfs["-t", "ext3", "/dev/sda1"] as p1: with mkfs["-t", "ext3", "/dev/sdb1"] as p2: pass .. note:: When processes run in the **background** (either via ``popen`` or :class:`& BG `), their stdout/stderr pipes might fill up, causing them to hang. If you know a process produces output, be sure to consume it every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ For the arguments, see :func:`run `. :returns: A Popen object, augmented with a ``.run()`` method, which returns a tuple of (return code, stdout, stderr) """ retcode = kwargs.pop("retcode", 0) timeout = kwargs.pop("timeout", None) p = self.popen(args, **kwargs) was_run = [False] def runner(): if was_run[0]: return None # already done was_run[0] = True try: return run_proc(p, retcode, timeout) finally: del p.run # to break cyclic reference p -> cell -> p for f in (p.stdin, p.stdout, p.stderr): with contextlib.suppress(Exception): f.close() p.run = runner yield p runner() def run(self, args=(), **kwargs): """Runs the given command (equivalent to popen() followed by :func:`run_proc `). If the exit code of the process does not match the expected one, :class:`ProcessExecutionError ` is raised. :param args: Any arguments to be passed to the process (a tuple) :param retcode: The expected return code of this process (defaults to 0). In order to disable exit-code validation, pass ``None``. It may also be a tuple (or any iterable) of expected exit codes. .. note:: this argument must be passed as a keyword argument. :param timeout: The maximal amount of time (in seconds) to allow the process to run. ``None`` means no timeout is imposed; otherwise, if the process hasn't terminated after that many seconds, the process will be forcefully terminated an exception will be raised .. note:: this argument must be passed as a keyword argument. :param kwargs: Any keyword-arguments to be passed to the ``Popen`` constructor :returns: A tuple of (return code, stdout, stderr) """ with self.bgrun(args, **kwargs) as p: return p.run() def _use_modifier(self, modifier, args): """ Applies a modifier to the current object (e.g. FG, NOHUP) :param modifier: The modifier class to apply (e.g. FG) :param args: A dictionary of arguments to pass to this modifier :return: """ modifier_instance = modifier(**args) return self & modifier_instance def run_bg(self, **kwargs): """ Run this command in the background. Uses all arguments from the BG construct :py:class: `plumbum.commands.modifiers.BG` """ return self._use_modifier(plumbum.commands.modifiers.BG, kwargs) def run_fg(self, **kwargs): """ Run this command in the foreground. Uses all arguments from the FG construct :py:class: `plumbum.commands.modifiers.FG` """ return self._use_modifier(plumbum.commands.modifiers.FG, kwargs) def run_tee(self, **kwargs): """ Run this command using the TEE construct. Inherits all arguments from TEE :py:class: `plumbum.commands.modifiers.TEE` """ return self._use_modifier(plumbum.commands.modifiers.TEE, kwargs) def run_tf(self, **kwargs): """ Run this command using the TF construct. Inherits all arguments from TF :py:class: `plumbum.commands.modifiers.TF` """ return self._use_modifier(plumbum.commands.modifiers.TF, kwargs) def run_retcode(self, **kwargs): """ Run this command using the RETCODE construct. Inherits all arguments from RETCODE :py:class: `plumbum.commands.modifiers.RETCODE` """ return self._use_modifier(plumbum.commands.modifiers.RETCODE, kwargs) def run_nohup(self, **kwargs): """ Run this command using the NOHUP construct. Inherits all arguments from NOHUP :py:class: `plumbum.commands.modifiers.NOHUP` """ return self._use_modifier(plumbum.commands.modifiers.NOHUP, kwargs) class BoundCommand(BaseCommand): __slots__ = ("cmd", "args") def __init__(self, cmd, args): self.cmd = cmd self.args = list(args) def __repr__(self): return f"BoundCommand({self.cmd!r}, {self.args!r})" def _get_encoding(self): return self.cmd._get_encoding() def formulate(self, level=0, args=()): return self.cmd.formulate(level + 1, self.args + list(args)) @property def machine(self): return self.cmd.machine def popen(self, args=(), **kwargs): if isinstance(args, str): args = [ args, ] return self.cmd.popen(self.args + list(args), **kwargs) class BoundEnvCommand(BaseCommand): __slots__ = ("cmd",) def __init__(self, cmd, env=None, cwd=None): self.cmd = cmd self.env = env or {} self.cwd = cwd def __repr__(self): return f"BoundEnvCommand({self.cmd!r}, {self.env!r})" def _get_encoding(self): return self.cmd._get_encoding() def formulate(self, level=0, args=()): return self.cmd.formulate(level, args) @property def machine(self): return self.cmd.machine def popen(self, args=(), cwd=None, env=None, **kwargs): env = env or {} return self.cmd.popen( args, cwd=self.cwd if cwd is None else cwd, env=dict(self.env, **env), **kwargs, ) class Pipeline(BaseCommand): __slots__ = ("srccmd", "dstcmd") def __init__(self, srccmd, dstcmd): self.srccmd = srccmd self.dstcmd = dstcmd def __repr__(self): return f"Pipeline({self.srccmd!r}, {self.dstcmd!r})" def _get_encoding(self): 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), ] @property def machine(self): return self.srccmd.machine def popen(self, args=(), **kwargs): src_kwargs = kwargs.copy() src_kwargs["stdout"] = PIPE if "stdin" in kwargs: src_kwargs["stdin"] = kwargs["stdin"] srcproc = self.srccmd.popen(args, **src_kwargs) kwargs["stdin"] = srcproc.stdout dstproc = self.dstcmd.popen(**kwargs) # allow p1 to receive a SIGPIPE if p2 exits srcproc.stdout.close() if srcproc.stdin and src_kwargs.get("stdin") != PIPE: srcproc.stdin.close() dstproc.srcproc = srcproc # monkey-patch .wait() to wait on srcproc as well (it's expected to die when dstproc dies) dstproc_wait = dstproc.wait @functools.wraps(Popen.wait) def wait2(*args, **kwargs): rc_dst = dstproc_wait(*args, **kwargs) rc_src = srcproc.wait(*args, **kwargs) dstproc.returncode = rc_dst or rc_src return dstproc.returncode dstproc._proc.wait = wait2 dstproc_verify = dstproc.verify def verify(proc, retcode, timeout, stdout, stderr): # TODO: right now it's impossible to specify different expected # return codes for different stages of the pipeline try: or_retcode = [0, *list(retcode)] except TypeError: # 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) dstproc.verify = MethodType(verify, dstproc) dstproc.stdin = srcproc.stdin return dstproc class BaseRedirection(BaseCommand): __slots__ = ("cmd", "file") SYM: ClassVar[str] KWARG: ClassVar[str] MODE: ClassVar[str] def __init__(self, cmd, file): self.cmd = cmd self.file = file def _get_encoding(self): return self.cmd._get_encoding() def __repr__(self): return f"{self.__class__.__name__}({self.cmd!r}, {self.file!r})" def formulate(self, level=0, args=()): return [ *self.cmd.formulate(level + 1, args), self.SYM, shquote(getattr(self.file, "name", self.file)), ] @property def machine(self): return self.cmd.machine def popen(self, args=(), **kwargs): from plumbum.machines.local import LocalPath from plumbum.machines.remote import RemotePath if self.KWARG in kwargs and kwargs[self.KWARG] not in (PIPE, None): raise RedirectionError(f"{self.KWARG} is already redirected") if isinstance(self.file, RemotePath): raise TypeError("Cannot redirect to/from remote paths") if isinstance(self.file, (str, LocalPath)): f = kwargs[self.KWARG] = open(str(self.file), self.MODE, encoding="utf-8") else: kwargs[self.KWARG] = self.file f = None try: return self.cmd.popen(args, **kwargs) finally: if f: f.close() class StdinRedirection(BaseRedirection): __slots__ = () SYM = "<" KWARG = "stdin" MODE = "r" class StdoutRedirection(BaseRedirection): __slots__ = () SYM = ">" KWARG = "stdout" MODE = "w" class AppendingStdoutRedirection(BaseRedirection): __slots__ = () SYM = ">>" KWARG = "stdout" MODE = "a" class StderrRedirection(BaseRedirection): __slots__ = () SYM = "2>" KWARG = "stderr" MODE = "w" class _ERROUT(int): def __repr__(self): return "ERROUT" def __str__(self): return "&1" ERROUT = _ERROUT(subprocess.STDOUT) class StdinDataRedirection(BaseCommand): __slots__ = ("cmd", "data") CHUNK_SIZE = 16000 def __init__(self, cmd, data): self.cmd = cmd self.data = data def _get_encoding(self): return self.cmd._get_encoding() def formulate(self, level=0, args=()): return [ f"echo {shquote(self.data)}", "|", *self.cmd.formulate(level + 1, args), ] @property def machine(self): return self.cmd.machine def popen(self, args=(), **kwargs): if kwargs.get("stdin") not in (PIPE, None): raise RedirectionError("stdin is already redirected") data = self.data if isinstance(data, str) and self._get_encoding() is not None: data = data.encode(self._get_encoding()) f = TemporaryFile() while data: chunk = data[: self.CHUNK_SIZE] f.write(chunk) data = data[self.CHUNK_SIZE :] f.seek(0) kwargs["stdin"] = f # try: return self.cmd.popen(args, **kwargs) # finally: # f.close() class ConcreteCommand(BaseCommand): QUOTE_LEVEL: ClassVar[int] __slots__ = ("executable",) def __init__(self, executable, encoding): self.executable = executable self.custom_encoding = encoding self.cwd = None self.env = None def __str__(self): return str(self.executable) def __repr__(self): return f"{type(self).__name__}({self.executable})" def _get_encoding(self): return self.custom_encoding def formulate(self, level=0, args=()): argv = [str(self.executable)] for a in args: if a is None: continue if isinstance(a, BaseCommand): if level >= self.QUOTE_LEVEL: argv.extend(shquote_list(a.formulate(level + 1))) else: argv.extend(a.formulate(level + 1)) elif isinstance(a, (list, tuple)): argv.extend( shquote(b) if level >= self.QUOTE_LEVEL else str(b) for b in a ) else: argv.append(shquote(a) if level >= self.QUOTE_LEVEL else str(a)) # if self.custom_encoding: # argv = [a.encode(self.custom_encoding) for a in argv if isinstance(a, str)] return argv @property def machine(self): raise NotImplementedError() def popen(self, args=(), **kwargs): raise NotImplementedError() plumbum-1.8.3/plumbum/commands/daemons.py0000644000000000000000000000672714613634536015443 0ustar00import contextlib import errno import os import signal import subprocess import sys import time import traceback from plumbum.commands.processes import ProcessExecutionError class _fake_lock: """Needed to allow normal os.exit() to work without error""" @staticmethod def acquire(_): return True @staticmethod def release(): pass def posix_daemonize(command, cwd, stdout=None, stderr=None, append=True): if stdout is None: stdout = os.devnull if stderr is None: stderr = stdout MAX_SIZE = 16384 rfd, wfd = os.pipe() argv = command.formulate() firstpid = os.fork() if firstpid == 0: # first child: become session leader os.close(rfd) rc = 0 try: os.setsid() os.umask(0) stdin = open(os.devnull, encoding="utf-8") stdout = open(stdout, "a" if append else "w", encoding="utf-8") stderr = open(stderr, "a" if append else "w", encoding="utf-8") signal.signal(signal.SIGHUP, signal.SIG_IGN) proc = command.popen( cwd=cwd, close_fds=True, stdin=stdin.fileno(), stdout=stdout.fileno(), stderr=stderr.fileno(), ) os.write(wfd, str(proc.pid).encode("utf8")) except Exception: rc = 1 tbtext = "".join(traceback.format_exception(*sys.exc_info()))[-MAX_SIZE:] os.write(wfd, tbtext.encode("utf8")) finally: os.close(wfd) os._exit(rc) # wait for first child to die os.close(wfd) _, rc = os.waitpid(firstpid, 0) output = os.read(rfd, MAX_SIZE) os.close(rfd) with contextlib.suppress(UnicodeError): output = output.decode("utf8") if rc == 0 and output.isdigit(): secondpid = int(output) else: raise ProcessExecutionError(argv, rc, "", output) proc = subprocess.Popen.__new__(subprocess.Popen) proc._child_created = True proc.returncode = None proc.stdout = None proc.stdin = None proc.stderr = None proc.pid = secondpid proc.universal_newlines = False proc._input = None proc._waitpid_lock = _fake_lock() proc._communication_started = False proc.args = argv proc.argv = argv def poll(self=proc): if self.returncode is None: try: os.kill(self.pid, 0) except OSError as ex: if ex.errno == errno.ESRCH: # process does not exist self.returncode = 0 else: raise return self.returncode def wait(self=proc): while self.returncode is None: if self.poll() is None: time.sleep(0.5) return proc.returncode proc.poll = poll proc.wait = wait return proc def win32_daemonize(command, cwd, stdout=None, stderr=None, append=True): if stdout is None: stdout = os.devnull if stderr is None: stderr = stdout DETACHED_PROCESS = 0x00000008 stdin = open(os.devnull, encoding="utf-8") stdout = open(stdout, "a" if append else "w", encoding="utf-8") stderr = open(stderr, "a" if append else "w", encoding="utf-8") return command.popen( cwd=cwd, stdin=stdin.fileno(), stdout=stdout.fileno(), stderr=stderr.fileno(), creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS, ) plumbum-1.8.3/plumbum/commands/modifiers.py0000644000000000000000000004130214613634536015762 0ustar00import sys from logging import DEBUG, INFO from select import select from subprocess import PIPE import plumbum.commands.base from plumbum.commands.processes import BY_TYPE, ProcessExecutionError, run_proc from plumbum.lib import read_fd_decode_safely class Future: """Represents a "future result" of a running process. It basically wraps a ``Popen`` object and the expected exit code, and provides poll(), wait(), returncode, stdout, and stderr. """ def __init__(self, proc, expected_retcode, timeout=None): self.proc = proc self._expected_retcode = expected_retcode self._timeout = timeout self._returncode = None self._stdout = None self._stderr = None def __repr__(self): running = self._returncode if self.ready() else "running" return f"" def poll(self): """Polls the underlying process for termination; returns ``False`` if still running, or ``True`` if terminated""" if self.proc.poll() is not None: self.wait() return self._returncode is not None ready = poll def wait(self): """Waits for the process to terminate; will raise a :class:`plumbum.commands.ProcessExecutionError` in case of failure""" if self._returncode is not None: return self._returncode, self._stdout, self._stderr = run_proc( self.proc, self._expected_retcode, self._timeout ) @property def stdout(self): """The process' stdout; accessing this property will wait for the process to finish""" self.wait() return self._stdout @property def stderr(self): """The process' stderr; accessing this property will wait for the process to finish""" self.wait() return self._stderr @property def returncode(self): """The process' returncode; accessing this property will wait for the process to finish""" self.wait() return self._returncode # =================================================================================================== # execution modifiers # =================================================================================================== class ExecutionModifier: __slots__ = ("__weakref__",) def __repr__(self): """Automatically creates a representation for given subclass with slots. Ignore hidden properties.""" slots = {} for cls in self.__class__.__mro__: slots_list = getattr(cls, "__slots__", ()) if isinstance(slots_list, str): slots_list = (slots_list,) for prop in slots_list: if prop[0] != "_": slots[prop] = getattr(self, prop) mystrs = (f"{name} = {value}" for name, value in slots.items()) mystrs_str = ", ".join(mystrs) return f"{self.__class__.__name__}({mystrs_str})" @classmethod def __call__(cls, *args, **kwargs): return cls(*args, **kwargs) class _BG(ExecutionModifier): """ An execution modifier that runs the given command in the background, returning a :class:`Future ` object. In order to mimic shell syntax, it applies when you right-and it with a command. If you wish to expect a different return code (other than the normal success indicate by 0), use ``BG(retcode)``. Example:: future = sleep[5] & BG # a future expecting an exit code of 0 future = sleep[5] & BG(7) # a future expecting an exit code of 7 .. note:: When processes run in the **background** (either via ``popen`` or :class:`& BG `), their stdout/stderr pipes might fill up, causing them to hang. If you know a process produces output, be sure to consume it every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ """ __slots__ = ("retcode", "kargs", "timeout") def __init__(self, retcode=0, timeout=None, **kargs): self.retcode = retcode self.kargs = kargs self.timeout = timeout def __rand__(self, cmd): return Future(cmd.popen(**self.kargs), self.retcode, timeout=self.timeout) class _FG(ExecutionModifier): """ An execution modifier that runs the given command in the foreground, passing it the current process' stdin, stdout and stderr. Useful for interactive programs that require a TTY. There is no return value. In order to mimic shell syntax, it applies when you right-and it with a command. If you wish to expect a different return code (other than the normal success indicate by 0), use ``FG(retcode)``. Example:: vim & FG # run vim in the foreground, expecting an exit code of 0 vim & FG(7) # run vim in the foreground, expecting an exit code of 7 """ __slots__ = ("retcode", "timeout") def __init__(self, retcode=0, timeout=None): self.retcode = retcode self.timeout = timeout def __rand__(self, cmd): cmd( retcode=self.retcode, stdin=None, stdout=None, stderr=None, timeout=self.timeout, ) class _TEE(ExecutionModifier): """Run a command, dumping its stdout/stderr to the current process's stdout and stderr, but ALSO return them. Useful for interactive programs that expect a TTY but also have valuable output. Use as: ls["-l"] & TEE Returns a tuple of (return code, stdout, stderr), just like ``run()``. """ __slots__ = ("retcode", "buffered", "timeout") def __init__(self, retcode=0, buffered=True, timeout=None): """`retcode` is the return code to expect to mean "success". Set `buffered` to False to disable line-buffering the output, which may cause stdout and stderr to become more entangled than usual. """ self.retcode = retcode self.buffered = buffered self.timeout = timeout def __rand__(self, cmd): with cmd.bgrun( retcode=self.retcode, stdin=None, stdout=PIPE, stderr=PIPE, timeout=self.timeout, ) as p: outbuf = [] errbuf = [] out = p.stdout err = p.stderr buffers = {out: outbuf, err: errbuf} tee_to = {out: sys.stdout, err: sys.stderr} done = False while not done: # After the process exits, we have to do one more # round of reading in order to drain any data in the # pipe buffer. Thus, we check poll() here, # unconditionally enter the read loop, and only then # break out of the outer loop if the process has # exited. done = p.poll() is not None # We continue this loop until we've done a full # `select()` call without collecting any input. This # ensures that our final pass -- after process exit -- # actually drains the pipe buffers, even if it takes # multiple calls to read(). progress = True while progress: progress = False ready, _, _ = select((out, err), (), ()) for fd in ready: buf = buffers[fd] data, text = read_fd_decode_safely(fd, 4096) if not data: # eof continue progress = True # Python conveniently line-buffers stdout and stderr for # us, so all we need to do is write to them # This will automatically add up to three bytes if it cannot be decoded tee_to[fd].write(text) # And then "unbuffered" is just flushing after each write if not self.buffered: tee_to[fd].flush() buf.append(data) p.wait() # To get return code in p stdout = "".join([x.decode("utf-8") for x in outbuf]) stderr = "".join([x.decode("utf-8") for x in errbuf]) return p.returncode, stdout, stderr class _TF(ExecutionModifier): """ An execution modifier that runs the given command, but returns True/False depending on the retcode. This returns True if the expected exit code is returned, and false if it is not. This is useful for checking true/false bash commands. If you wish to expect a different return code (other than the normal success indicate by 0), use ``TF(retcode)``. If you want to run the process in the foreground, then use ``TF(FG=True)``. Example:: local['touch']['/root/test'] & TF * Returns False, since this cannot be touched local['touch']['/root/test'] & TF(1) # Returns True local['touch']['/root/test'] & TF(FG=True) * Returns False, will show error message """ __slots__ = ("retcode", "FG", "timeout") def __init__( self, retcode=0, FG=False, # pylint: disable=redefined-outer-name timeout=None, ): """`retcode` is the return code to expect to mean "success". Set `FG` to True to run in the foreground. """ self.retcode = retcode self.FG = FG self.timeout = timeout @classmethod def __call__(cls, *args, **kwargs): return cls(*args, **kwargs) def __rand__(self, cmd): try: if self.FG: cmd( retcode=self.retcode, stdin=None, stdout=None, stderr=None, timeout=self.timeout, ) else: cmd(retcode=self.retcode, timeout=self.timeout) return True except ProcessExecutionError: return False class _RETCODE(ExecutionModifier): """ An execution modifier that runs the given command, causing it to run and return the retcode. This is useful for working with bash commands that have important retcodes but not very useful output. If you want to run the process in the foreground, then use ``RETCODE(FG=True)``. Example:: local['touch']['/root/test'] & RETCODE # Returns 1, since this cannot be touched local['touch']['/root/test'] & RETCODE(FG=True) * Returns 1, will show error message """ __slots__ = ("foreground", "timeout") def __init__( self, FG=False, # pylint: disable=redefined-outer-name timeout=None, ): """`FG` to True to run in the foreground.""" self.foreground = FG self.timeout = timeout @classmethod def __call__(cls, *args, **kwargs): return cls(*args, **kwargs) def __rand__(self, cmd): if self.foreground: result = cmd.run( retcode=None, stdin=None, stdout=None, stderr=None, timeout=self.timeout ) return result[0] return cmd.run(retcode=None, timeout=self.timeout)[0] class _NOHUP(ExecutionModifier): """ An execution modifier that runs the given command in the background, disconnected 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 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. Example:: sleep[5] & NOHUP # Outputs to nohup.out sleep[5] & NOHUP(stdout=os.devnull) # No output The equivalent bash command would be .. code-block:: bash nohup sleep 5 & """ __slots__ = ("cwd", "stdout", "stderr", "append") def __init__(self, cwd=".", stdout="nohup.out", stderr=None, append=True): """Set ``cwd``, ``stdout``, or ``stderr``. Runs as a forked process. You can set ``append=False``, too. """ self.cwd = cwd self.stdout = stdout self.stderr = stderr self.append = append def __rand__(self, cmd): if isinstance(cmd, plumbum.commands.base.StdoutRedirection): stdout = cmd.file append = False cmd = cmd.cmd elif isinstance(cmd, plumbum.commands.base.AppendingStdoutRedirection): stdout = cmd.file append = True cmd = cmd.cmd else: stdout = self.stdout append = self.append return cmd.nohup(self.cwd, stdout, self.stderr, append) class LogPipe: def __init__(self, line_timeout, kw, levels, prefix, log): self.line_timeout = line_timeout self.kw = kw self.levels = levels self.prefix = prefix self.log = log def __rand__(self, cmd): popen = cmd if hasattr(cmd, "iter_lines") else cmd.popen() for typ, lines in popen.iter_lines( line_timeout=self.line_timeout, mode=BY_TYPE, **self.kw ): if not lines: continue level = self.levels[typ] for line in lines.splitlines(): if self.prefix: line = f"{self.prefix}: {line}" # noqa: PLW2901 self.log(level, line) return popen.returncode class PipeToLoggerMixin: """ This mixin allows piping plumbum commands' output into a logger. The logger must implement a ``log(level, msg)`` method, as in ``logging.Logger`` Example:: class MyLogger(logging.Logger, PipeToLoggerMixin): pass logger = MyLogger("example.app") Here we send the output of an install.sh script into our log:: local['./install.sh'] & logger We can choose the log-level for each stream:: local['./install.sh'] & logger.pipe(out_level=logging.DEBUG, err_level=logging.DEBUG) Or use a convenience method for it:: local['./install.sh'] & logger.pipe_debug() A prefix can be added to each line:: local['./install.sh'] & logger.pipe(prefix="install.sh: ") If the command fails, an exception is raised as usual. This can be modified:: local['install.sh'] & logger.pipe_debug(retcode=None) An exception is also raised if too much time (``DEFAULT_LINE_TIMEOUT``) passed between lines in the stream, This can also be modified:: local['install.sh'] & logger.pipe(line_timeout=10) If we happen to use logbook:: class MyLogger(logbook.Logger, PipeToLoggerMixin): from logbook import DEBUG, INFO # hook up with logbook's levels """ DEFAULT_LINE_TIMEOUT = 10 * 60 DEFAULT_STDOUT = "INFO" DEFAULT_STDERR = "DEBUG" INFO = INFO DEBUG = DEBUG def pipe( self, out_level=None, err_level=None, prefix=None, line_timeout=None, **kw ): """ Pipe a command's stdout and stderr lines into this logger. :param out_level: the log level for lines coming from stdout :param err_level: the log level for lines coming from stderr Optionally use `prefix` for each line. """ levels = { 1: getattr(self, self.DEFAULT_STDOUT), 2: getattr(self, self.DEFAULT_STDERR), } if line_timeout is None: line_timeout = self.DEFAULT_LINE_TIMEOUT if out_level is not None: levels[1] = out_level if err_level is not None: levels[2] = err_level return LogPipe(line_timeout, kw, levels, prefix, self.log) def pipe_info(self, prefix=None, **kw): """ Pipe a command's stdout and stderr lines into this logger (both at level INFO) """ return self.pipe(self.INFO, self.INFO, prefix=prefix, **kw) def pipe_debug(self, prefix=None, **kw): """ Pipe a command's stdout and stderr lines into this logger (both at level DEBUG) """ return self.pipe(self.DEBUG, self.DEBUG, prefix=prefix, **kw) def __rand__(self, cmd): """ Pipe a command's stdout and stderr lines into this logger. Log levels for each stream are determined by ``DEFAULT_STDOUT`` and ``DEFAULT_STDERR``. """ return cmd & self.pipe( getattr(self, self.DEFAULT_STDOUT), getattr(self, self.DEFAULT_STDERR) ) BG = _BG() FG = _FG() NOHUP = _NOHUP() RETCODE = _RETCODE() TEE = _TEE() TF = _TF() plumbum-1.8.3/plumbum/commands/processes.py0000644000000000000000000003530714613634536016017 0ustar00import atexit import contextlib import heapq import math import time from queue import Empty as QueueEmpty from queue import Queue from threading import Thread from plumbum.lib import IS_WIN32 # =================================================================================================== # utility functions # =================================================================================================== def _check_process(proc, retcode, timeout, stdout, stderr): proc.verify(retcode, timeout, stdout, stderr) return proc.returncode, stdout, stderr def _get_piped_streams(proc): """Get a list of all valid standard streams for proc that were opened with PIPE option. If proc was started from a Pipeline command, this function assumes it will have a "srcproc" member pointing to the previous command in the pipeline. That link will be used to traverse all started processes started from the pipeline, the list will include stdout/stderr streams opened as PIPE for all commands in the pipeline. If that was not the case, some processes could write to pipes no one reads from which would result in process stalling after the pipe's buffer is filled. Streams that were closed (because they were redirected to the input of a subsequent command) are not included in the result """ streams = [] def add_stream(type_, stream): if stream is None or stream.closed: return streams.append((type_, stream)) while proc: add_stream(1, proc.stderr) add_stream(0, proc.stdout) proc = getattr(proc, "srcproc", None) return streams def _iter_lines_posix(proc, decode, linesize, line_timeout=None): from selectors import EVENT_READ, DefaultSelector streams = _get_piped_streams(proc) # Python 3.4+ implementation def selector(): sel = DefaultSelector() for stream_type, stream in streams: sel.register(stream, EVENT_READ, stream_type) while True: ready = sel.select(line_timeout) if not ready and line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) for key, _mask in ready: yield key.data, decode(key.fileobj.readline(linesize)) for ret in selector(): yield ret if proc.poll() is not None: break for stream_type, stream in streams: for line in stream: yield stream_type, decode(line) def _iter_lines_win32(proc, decode, linesize, line_timeout=None): class Piper(Thread): def __init__(self, fd, pipe): super().__init__(name=f"PlumbumPiper{fd}Thread") self.pipe = pipe self.fd = fd self.empty = False self.daemon = True super().start() def read_from_pipe(self): return self.pipe.readline(linesize) def run(self): for line in iter(self.read_from_pipe, b""): queue.put((self.fd, decode(line))) # self.pipe.close() if line_timeout is None: line_timeout = float("inf") queue = Queue() pipers = [Piper(0, proc.stdout), Piper(1, proc.stderr)] last_line_ts = time.time() empty = True while True: try: yield queue.get_nowait() last_line_ts = time.time() empty = False except QueueEmpty: empty = True if time.time() - last_line_ts > line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) if proc.poll() is not None: break if empty: time.sleep(0.1) for piper in pipers: piper.join() while True: try: yield queue.get_nowait() except QueueEmpty: break _iter_lines = _iter_lines_win32 if IS_WIN32 else _iter_lines_posix # =================================================================================================== # Exceptions # =================================================================================================== class ProcessExecutionError(OSError): """Represents the failure of a process. When the exit code of a terminated process does not match the expected result, this exception is raised by :func:`run_proc `. It contains the process' return code, stdout, and stderr, as well as the command line used to create the process (``argv``) """ 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 failing in loading this object from a pickle.dumps. # pylint: disable-next=non-parent-init-called Exception.__init__(self, argv, retcode, stdout, stderr) self.message = message self.host = host self.argv = argv self.retcode = retcode if isinstance(stdout, bytes): stdout = ascii(stdout) if isinstance(stderr, bytes): stderr = ascii(stderr) self.stdout = stdout self.stderr = stderr def __str__(self): # avoid an import cycle from plumbum.commands.base import shquote_list stdout = "\n | ".join(str(self.stdout).splitlines()) stderr = "\n | ".join(str(self.stderr).splitlines()) cmd = " ".join(shquote_list(self.argv)) lines = [] if self.message: lines = [self.message, "\nReturn code: | ", str(self.retcode)] else: lines = ["Unexpected exit code: ", str(self.retcode)] cmd = "\n | ".join(cmd.splitlines()) lines += ["\nCommand line: | ", cmd] if self.host: lines += ["\nHost: | ", self.host] if stdout: lines += ["\nStdout: | ", stdout] if stderr: lines += ["\nStderr: | ", stderr] return "".join(lines) class ProcessTimedOut(Exception): """Raises by :func:`run_proc ` when a ``timeout`` has been specified and it has elapsed before the process terminated""" def __init__(self, msg, argv): Exception.__init__(self, msg, argv) self.argv = argv class ProcessLineTimedOut(Exception): """Raises by :func:`iter_lines ` when a ``line_timeout`` has been specified and it has elapsed before the process yielded another line""" def __init__(self, msg, argv, machine): Exception.__init__(self, msg, argv, machine) self.argv = argv self.machine = machine class CommandNotFound(AttributeError): """Raised by :func:`local.which ` and :func:`RemoteMachine.which ` when a command was not found in the system's ``PATH``""" def __init__(self, program, path): super().__init__(self, program, path) self.program = program self.path = path # =================================================================================================== # Timeout thread # =================================================================================================== class MinHeap: def __init__(self, items=()): self._items = list(items) heapq.heapify(self._items) def __len__(self): return len(self._items) def push(self, item): heapq.heappush(self._items, item) def pop(self): heapq.heappop(self._items) def peek(self): return self._items[0] _timeout_queue = Queue() # type: ignore[var-annotated] _shutting_down = False def _timeout_thread_func(): waiting = MinHeap() try: while not _shutting_down: if waiting: ttk, _ = waiting.peek() timeout = max(0, ttk - time.time()) else: timeout = None with contextlib.suppress(QueueEmpty): proc, time_to_kill = _timeout_queue.get(timeout=timeout) if proc is SystemExit: # terminate return waiting.push((time_to_kill, proc)) now = time.time() while waiting: ttk, proc = waiting.peek() if ttk > now: break waiting.pop() with contextlib.suppress(OSError): if proc.poll() is None: proc.kill() proc._timed_out = True except Exception: if _shutting_down: # to prevent all sorts of exceptions during interpreter shutdown pass else: raise bgthd = Thread(target=_timeout_thread_func, name="PlumbumTimeoutThread") bgthd.daemon = True bgthd.start() def _register_proc_timeout(proc, timeout): if timeout is not None: _timeout_queue.put((proc, time.time() + timeout)) def _shutdown_bg_threads(): 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 if _timeout_queue: # type: ignore[truthy-bool] _timeout_queue.put((SystemExit, 0)) # grace period bgthd.join(0.1) atexit.register(_shutdown_bg_threads) # =================================================================================================== # run_proc # =================================================================================================== def run_proc(proc, retcode, timeout=None): """Waits for the given process to terminate, with the expected exit code :param proc: a running Popen-like object, with all the expected methods. :param retcode: the expected return (exit) code of the process. It defaults to 0 (the convention for success). If ``None``, the return code is ignored. It may also be a tuple (or any object that supports ``__contains__``) of expected return codes. :param timeout: the number of seconds (a ``float``) to allow the process to run, before forcefully terminating it. If ``None``, not timeout is imposed; otherwise the process is expected to terminate within that timeout value, or it will be killed and :class:`ProcessTimedOut ` will be raised :returns: A tuple of (return code, stdout, stderr) """ _register_proc_timeout(proc, timeout) stdout, stderr = proc.communicate() proc._end_time = time.time() if not stdout: stdout = b"" if not stderr: stderr = b"" if getattr(proc, "custom_encoding", None): stdout = stdout.decode(proc.custom_encoding, "ignore") stderr = stderr.decode(proc.custom_encoding, "ignore") return _check_process(proc, retcode, timeout, stdout, stderr) # =================================================================================================== # iter_lines # =================================================================================================== BY_POSITION = object() BY_TYPE = object() DEFAULT_ITER_LINES_MODE = BY_POSITION DEFAULT_BUFFER_SIZE = math.inf def iter_lines( proc, retcode=0, timeout=None, linesize=-1, line_timeout=None, buffer_size=None, mode=None, _iter_lines=_iter_lines, ): """Runs the given process (equivalent to run_proc()) and yields a tuples of (out, err) line pairs. If the exit code of the process does not match the expected one, :class:`ProcessExecutionError ` is raised. :param retcode: The expected return code of this process (defaults to 0). In order to disable exit-code validation, pass ``None``. It may also be a tuple (or any iterable) of expected exit codes. :param timeout: The maximal amount of time (in seconds) to allow the process to run. ``None`` means no timeout is imposed; otherwise, if the process hasn't terminated after that many seconds, the process will be forcefully terminated an exception will be raised :param linesize: Maximum number of characters to read from stdout/stderr at each iteration. ``-1`` (default) reads until a b'\\n' is encountered. :param line_timeout: The maximal amount of time (in seconds) to allow between consecutive lines in either stream. Raise an :class:`ProcessLineTimedOut ` if the timeout has been reached. ``None`` means no timeout is imposed. :param buffer_size: Maximum number of lines to keep in the stdout/stderr buffers, in case of a ProcessExecutionError. Default is ``None``, which defaults to DEFAULT_BUFFER_SIZE (which is infinite by default). ``0`` will disable buffering completely. :param mode: Controls what the generator yields. Defaults to DEFAULT_ITER_LINES_MODE (which is BY_POSITION by default) - BY_POSITION (default): yields ``(out, err)`` line tuples, where either item may be ``None`` - BY_TYPE: yields ``(fd, line)`` tuples, where ``fd`` is 1 (stdout) or 2 (stderr) :returns: An iterator of (out, err) line tuples. """ if mode is None: mode = DEFAULT_ITER_LINES_MODE if buffer_size is None: buffer_size = DEFAULT_BUFFER_SIZE buffer_size: int # type: ignore[annotation-unchecked] assert mode in (BY_POSITION, BY_TYPE) encoding = getattr(proc, "custom_encoding", None) or "utf-8" decode = lambda s: s.decode(encoding, errors="replace").rstrip() # noqa: E731 _register_proc_timeout(proc, timeout) 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) buffer = buffers[t] if buffer_size > 0: buffer.append(line) if buffer_size < math.inf: del buffer[:-buffer_size] if mode is BY_POSITION: ret = [None, None] ret[t] = line yield tuple(ret) elif mode is BY_TYPE: yield (t + 1), line # 1=stdout, 2=stderr # this will take care of checking return code and timeouts _check_process(proc, retcode, timeout, *("\n".join(s) + "\n" for s in buffers)) plumbum-1.8.3/plumbum/fs/__init__.py0000644000000000000000000000004714613634536014350 0ustar00""" file-system related operations """ plumbum-1.8.3/plumbum/fs/atomic.py0000644000000000000000000002232114613634536014064 0ustar00""" Atomic file operations """ import atexit import contextlib import os import threading from plumbum.machines.local import local try: import fcntl except ImportError: import msvcrt try: from pywintypes import error as WinError from win32con import LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY from win32file import OVERLAPPED, LockFileEx, UnlockFile except ImportError: print( # noqa: T201 "On Windows, Plumbum requires Python for Windows Extensions (pywin32)" ) raise @contextlib.contextmanager def locked_file(fileno, blocking=True): hndl = msvcrt.get_osfhandle(fileno) try: LockFileEx( hndl, LOCKFILE_EXCLUSIVE_LOCK | (0 if blocking else LOCKFILE_FAIL_IMMEDIATELY), 0xFFFFFFFF, 0xFFFFFFFF, OVERLAPPED(), ) except WinError as ex: raise OSError(*ex.args) from None try: yield finally: UnlockFile(hndl, 0, 0, 0xFFFFFFFF, 0xFFFFFFFF) else: if hasattr(fcntl, "lockf"): @contextlib.contextmanager def locked_file(fileno, blocking=True): fcntl.lockf(fileno, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) try: yield finally: fcntl.lockf(fileno, fcntl.LOCK_UN) else: @contextlib.contextmanager def locked_file(fileno, blocking=True): fcntl.flock(fileno, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) try: yield finally: fcntl.flock(fileno, fcntl.LOCK_UN) class AtomicFile: """ Atomic file operations implemented using file-system advisory locks (``flock`` on POSIX, ``LockFile`` on Windows). .. note:: On Linux, the manpage says ``flock`` might have issues with NFS mounts. You should take this into account. .. versionadded:: 1.3 """ CHUNK_SIZE = 32 * 1024 def __init__(self, filename, ignore_deletion=False): self.path = local.path(filename) self._ignore_deletion = ignore_deletion self._thdlock = threading.Lock() self._owned_by = None self._fileobj = None self.reopen() def __repr__(self): return f"" if self._fileobj else "" def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): if self._fileobj is not None: self._fileobj.close() self._fileobj = None def reopen(self): """ Close and reopen the file; useful when the file was deleted from the file system by a different process """ self.close() self._fileobj = os.fdopen( os.open(str(self.path), os.O_CREAT | os.O_RDWR, 384), "r+b", 0 ) @contextlib.contextmanager def locked(self, blocking=True): """ A context manager that locks the file; this function is reentrant by the thread currently holding the lock. :param blocking: if ``True``, the call will block until we can grab the file system lock. if ``False``, the call may fail immediately with the underlying exception (``IOError`` or ``WindowsError``) """ if self._owned_by == threading.get_ident(): yield return 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): """ Atomically delete the file (holds the lock while doing it) """ with self.locked(): self.path.delete() def _read_all(self): self._fileobj.seek(0) data = [] while True: buf = self._fileobj.read(self.CHUNK_SIZE) data.append(buf) if len(buf) < self.CHUNK_SIZE: break return b"".join(data) def read_atomic(self): """Atomically read the entire file""" with self.locked(): return self._read_all() def read_shared(self): """Read the file **without** holding the lock""" return self._read_all() def write_atomic(self, data): """Writes the given data atomically to the file. Note that it overwrites the entire file; ``write_atomic("foo")`` followed by ``write_atomic("bar")`` will result in only ``"bar"``. """ with self.locked(): self._fileobj.seek(0) while data: chunk = data[: self.CHUNK_SIZE] self._fileobj.write(chunk) data = data[len(chunk) :] self._fileobj.flush() self._fileobj.truncate() class AtomicCounterFile: """ An atomic counter based on AtomicFile. Each time you call ``next()``, it will atomically read and increment the counter's value, returning its previous value Example:: acf = AtomicCounterFile.open("/some/file") print(acf.next()) # e.g., 7 print(acf.next()) # 8 print(acf.next()) # 9 .. versionadded:: 1.3 """ def __init__(self, atomicfile, initial=0): """ :param atomicfile: an :class:`AtomicFile ` instance :param initial: the initial value (used when the first time the file is created) """ self.atomicfile = atomicfile self.initial = initial def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): self.atomicfile.close() @classmethod def open(cls, filename): """ Shortcut for ``AtomicCounterFile(AtomicFile(filename))`` """ return cls(AtomicFile(filename)) def reset(self, value=None): """ Reset the counter's value to the one given. If ``None``, it will default to the initial value provided to the constructor """ if value is None: value = self.initial if not isinstance(value, int): raise TypeError(f"value must be an integer, not {type(value)!r}") self.atomicfile.write_atomic(str(value).encode("utf8")) def next(self): """ Read and increment the counter, returning its previous value """ with self.atomicfile.locked(): curr = self.atomicfile.read_atomic().decode("utf8") curr = self.initial if not curr else int(curr) self.atomicfile.write_atomic(str(curr + 1).encode("utf8")) return curr class PidFileTaken(SystemExit): """ This exception is raised when PidFile.acquire fails to lock the pid file. Note that it derives from ``SystemExit``, so unless explicitly handled, it will terminate the process cleanly """ def __init__(self, msg, pid): SystemExit.__init__(self, msg) self.pid = pid class PidFile: """ A PID file is a file that's locked by some process from the moment it starts until it dies (the OS will clear the lock when the process exits). It is used to prevent two instances of the same process (normally a daemon) from running concurrently. The PID file holds its process' PID, so you know who's holding it. .. versionadded:: 1.3 """ def __init__(self, filename): self.atomicfile = AtomicFile(filename) self._ctx = None def __enter__(self): self.acquire() def __exit__(self, t, v, tb): self.release() def __del__(self): with contextlib.suppress(Exception): self.release() def close(self): self.atomicfile.close() def acquire(self): """ Attempt to acquire the PID file. If it's already locked, raises :class:`PidFileTaken `. You should normally acquire the file as early as possible when the program starts """ if self._ctx is not None: return self._ctx = self.atomicfile.locked(blocking=False) try: self._ctx.__enter__() # pylint: disable=unnecessary-dunder-call except OSError: self._ctx = None try: pid = self.atomicfile.read_shared().strip().decode("utf8") except OSError: pid = "Unknown" raise PidFileTaken( f"PID file {self.atomicfile.path!r} taken by process {pid}", pid, ) from None self.atomicfile.write_atomic(str(os.getpid()).encode("utf8")) atexit.register(self.release) def release(self): """ Release the PID file (should only happen when the program terminates) """ if self._ctx is None: return self.atomicfile.delete() try: self._ctx.__exit__(None, None, None) finally: self._ctx = None plumbum-1.8.3/plumbum/fs/mounts.py0000644000000000000000000000211114613634536014130 0ustar00import re class MountEntry: """ Represents a mount entry (device file, mount point and file system type) """ def __init__(self, dev, point, fstype, options): self.dev = dev self.point = point self.fstype = fstype self.options = options.split(",") def __str__(self): options = ",".join(self.options) return f"{self.dev} on {self.point} type {self.fstype} ({options})" MOUNT_PATTERN = re.compile(r"(.+?)\s+on\s+(.+?)\s+type\s+(\S+)(?:\s+\((.+?)\))?") def mount_table(): """returns the system's current mount table (a list of :class:`MountEntry ` objects)""" from plumbum.cmd import mount table = [] for line in mount().splitlines(): m = MOUNT_PATTERN.match(line) if not m: continue table.append(MountEntry(*m.groups())) return table def mounted(fs): """ Indicates if a the given filesystem (device file or mount point) is currently mounted """ return any(fs in {entry.dev, entry.point} for entry in mount_table()) plumbum-1.8.3/plumbum/machines/__init__.py0000644000000000000000000000054414613634536015531 0ustar00from plumbum.machines.local import LocalCommand, LocalMachine, local from plumbum.machines.remote import BaseRemoteMachine, RemoteCommand from plumbum.machines.ssh_machine import PuttyMachine, SshMachine __all__ = ( "LocalCommand", "LocalMachine", "local", "BaseRemoteMachine", "RemoteCommand", "PuttyMachine", "SshMachine", ) plumbum-1.8.3/plumbum/machines/_windows.py0000644000000000000000000000132714613634536015623 0ustar00import struct LFANEW_OFFSET = 30 * 2 FILE_HEADER_SIZE = 5 * 4 SUBSYSTEM_OFFSET = 17 * 4 IMAGE_SUBSYSTEM_WINDOWS_GUI = 2 IMAGE_SUBSYSTEM_WINDOWS_CUI = 3 def get_pe_subsystem(filename): with open(filename, "rb") as f: if f.read(2) != b"MZ": return None f.seek(LFANEW_OFFSET) lfanew = struct.unpack("L", f.read(4))[0] f.seek(lfanew) if f.read(4) != b"PE\x00\x00": return None f.seek(FILE_HEADER_SIZE + SUBSYSTEM_OFFSET, 1) return struct.unpack("H", f.read(2))[0] # print(get_pe_subsystem("c:\\windows\\notepad.exe")) == 2 # print(get_pe_subsystem("c:\\python32\\python.exe")) == 3 # print(get_pe_subsystem("c:\\python32\\pythonw.exe")) == 2 plumbum-1.8.3/plumbum/machines/base.py0000644000000000000000000000650514613634536014707 0ustar00from plumbum.commands.processes import ( CommandNotFound, ProcessExecutionError, ProcessTimedOut, ) class PopenAddons: """This adds a verify to popen objects to that the correct command is attributed when an error is thrown.""" def verify(self, retcode, timeout, stdout, stderr): """This verifies that the correct command is attributed.""" if getattr(self, "_timed_out", False): raise ProcessTimedOut( f"Process did not terminate within {timeout} seconds", getattr(self, "argv", None), ) if retcode is not None: if hasattr(retcode, "__contains__"): if self.returncode not in retcode: raise ProcessExecutionError( getattr(self, "argv", None), self.returncode, stdout, stderr ) elif self.returncode != retcode: raise ProcessExecutionError( getattr(self, "argv", None), self.returncode, stdout, stderr ) class BaseMachine: """This is a base class for other machines. It contains common code to all machines in Plumbum.""" def get(self, cmd, *othercommands): """This works a little like the ``.get`` method with dict's, only it supports an unlimited number of arguments, since later arguments are tried as commands and could also fail. It will try to call the first command, and if that is not found, it will call the next, etc. Will raise if no file named for the executable if a path is given, unlike ``[]`` access. Usage:: best_zip = local.get('pigz','gzip') """ try: command = self[cmd] if not command.executable.exists(): raise CommandNotFound(cmd, command.executable) return command except CommandNotFound: if othercommands: return self.get(othercommands[0], *othercommands[1:]) raise def __contains__(self, cmd): """Tests for the existence of the command, e.g., ``"ls" in plumbum.local``. ``cmd`` can be anything acceptable by ``__getitem__``. """ try: self[cmd] except CommandNotFound: return False return True @property def encoding(self): "This is a wrapper for custom_encoding" return self.custom_encoding @encoding.setter def encoding(self, value): self.custom_encoding = value def daemonic_popen(self, command, cwd="/", stdout=None, stderr=None, append=True): raise NotImplementedError("This is not implemented on this machine!") class Cmd: def __init__(self, machine): self._machine = machine def __getattr__(self, name): try: return self._machine[name] except CommandNotFound: raise AttributeError(name) from None @property def cmd(self): return self.Cmd(self) def clear_program_cache(self): """ Clear the program cache, which is populated via ``machine.which(progname)`` calls. This cache speeds up the lookup of a program in the machines PATH, and is particularly effective for RemoteMachines. """ self._program_cache.clear() plumbum-1.8.3/plumbum/machines/env.py0000644000000000000000000001426714613634536014571 0ustar00import os from contextlib import contextmanager class EnvPathList(list): __slots__ = ["_path_factory", "_pathsep", "__weakref__"] def __init__(self, path_factory, pathsep): super().__init__() self._path_factory = path_factory self._pathsep = pathsep def append(self, path): list.append(self, self._path_factory(path)) def extend(self, paths): list.extend(self, (self._path_factory(p) for p in paths)) def insert(self, index, path): list.insert(self, index, self._path_factory(path)) def index(self, path): list.index(self, self._path_factory(path)) def __contains__(self, path): return list.__contains__(self, self._path_factory(path)) def remove(self, path): list.remove(self, self._path_factory(path)) def update(self, text): self[:] = [self._path_factory(p) for p in text.split(self._pathsep)] def join(self): return self._pathsep.join(str(p) for p in self) class BaseEnv: """The base class of LocalEnv and RemoteEnv""" __slots__ = ["_curr", "_path", "_path_factory", "__weakref__"] CASE_SENSITIVE = True def __init__(self, path_factory, pathsep, *, _curr): self._curr = _curr self._path_factory = path_factory self._path = EnvPathList(path_factory, pathsep) self._update_path() def _update_path(self): self._path.update(self.get("PATH", "")) @contextmanager def __call__(self, *args, **kwargs): """A context manager that can be used for temporal modifications of the environment. Any time you enter the context, a copy of the old environment is stored, and then restored, when the context exits. :param args: Any positional arguments for ``update()`` :param kwargs: Any keyword arguments for ``update()`` """ prev = self._curr.copy() self.update(**kwargs) try: yield finally: self._curr = prev self._update_path() def __iter__(self): """Returns an iterator over the items ``(key, value)`` of current environment (like dict.items)""" return iter(self._curr.items()) def __hash__(self): raise TypeError("unhashable type") def __len__(self): """Returns the number of elements of the current environment""" return len(self._curr) def __contains__(self, name): """Tests whether an environment variable exists in the current environment""" return (name if self.CASE_SENSITIVE else name.upper()) in self._curr def __getitem__(self, name): """Returns the value of the given environment variable from current environment, raising a ``KeyError`` if it does not exist""" return self._curr[name if self.CASE_SENSITIVE else name.upper()] def keys(self): """Returns the keys of the current environment (like dict.keys)""" return self._curr.keys() def items(self): """Returns the items of the current environment (like dict.items)""" return self._curr.items() def values(self): """Returns the values of the current environment (like dict.values)""" return self._curr.values() def get(self, name, *default): """Returns the keys of the current environment (like dict.keys)""" return self._curr.get((name if self.CASE_SENSITIVE else name.upper()), *default) def __delitem__(self, name): """Deletes an environment variable from the current environment""" name = name if self.CASE_SENSITIVE else name.upper() del self._curr[name] if name == "PATH": self._update_path() def __setitem__(self, name, value): """Sets/replaces an environment variable's value in the current environment""" name = name if self.CASE_SENSITIVE else name.upper() self._curr[name] = value if name == "PATH": self._update_path() def pop(self, name, *default): """Pops an element from the current environment (like dict.pop)""" name = name if self.CASE_SENSITIVE else name.upper() res = self._curr.pop(name, *default) if name == "PATH": self._update_path() return res def clear(self): """Clears the current environment (like dict.clear)""" self._curr.clear() self._update_path() def update(self, *args, **kwargs): """Updates the current environment (like dict.update)""" self._curr.update(*args, **kwargs) if not self.CASE_SENSITIVE: for k, v in list(self._curr.items()): self._curr[k.upper()] = v self._update_path() def getdict(self): """Returns the environment as a real dictionary""" self._curr["PATH"] = self.path.join() return {k: str(v) for k, v in self._curr.items()} @property def path(self): """The system's ``PATH`` (as an easy-to-manipulate list)""" return self._path def _get_home(self): if "HOME" in self: return self._path_factory(self["HOME"]) if "USERPROFILE" in self: # pragma: no cover return self._path_factory(self["USERPROFILE"]) if "HOMEPATH" in self: # pragma: no cover return self._path_factory(self.get("HOMEDRIVE", ""), self["HOMEPATH"]) return None def _set_home(self, p): if "HOME" in self: self["HOME"] = str(p) elif "USERPROFILE" in self: # pragma: no cover self["USERPROFILE"] = str(p) elif "HOMEPATH" in self: # pragma: no cover self["HOMEPATH"] = str(p) else: # pragma: no cover self["HOME"] = str(p) home = property(_get_home, _set_home) """Get or set the home path""" @property def user(self): """Return the user name, or ``None`` if it is not set""" # adapted from getpass.getuser() for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): # pragma: no branch if name in self: return self[name] try: # POSIX only import pwd except ImportError: return None return pwd.getpwuid(os.getuid())[0] # @UndefinedVariable plumbum-1.8.3/plumbum/machines/local.py0000644000000000000000000003702614613634536015071 0ustar00import contextlib import logging import os import platform import re import subprocess import sys import time from contextlib import contextmanager from subprocess import PIPE, Popen from tempfile import mkdtemp from typing import Dict, Tuple from plumbum.commands import CommandNotFound, ConcreteCommand from plumbum.commands.daemons import posix_daemonize, win32_daemonize from plumbum.commands.processes import iter_lines from plumbum.lib import IS_WIN32, ProcInfo, StaticProperty from plumbum.machines.base import BaseMachine, PopenAddons from plumbum.machines.env import BaseEnv from plumbum.machines.session import ShellSession from plumbum.path.local import LocalPath, LocalWorkdir from plumbum.path.remote import RemotePath class PlumbumLocalPopen(PopenAddons): iter_lines = iter_lines def __init__(self, *args, **kwargs): self._proc = Popen(*args, **kwargs) # pylint: disable=consider-using-with def __iter__(self): return self.iter_lines() def __enter__(self): return self._proc.__enter__() def __exit__(self, *args, **kwargs): return self._proc.__exit__(*args, **kwargs) def __getattr__(self, name): return getattr(self._proc, name) if IS_WIN32: from plumbum.machines._windows import IMAGE_SUBSYSTEM_WINDOWS_CUI, get_pe_subsystem logger = logging.getLogger("plumbum.local") # =================================================================================================== # Environment # =================================================================================================== class LocalEnv(BaseEnv): """The local machine's environment; exposes a dict-like interface""" __slots__ = () CASE_SENSITIVE = not IS_WIN32 def __init__(self): # os.environ already takes care of upper'ing on windows super().__init__(LocalPath, os.path.pathsep, _curr=os.environ.copy()) if IS_WIN32 and "HOME" not in self and self.home is not None: self["HOME"] = self.home def expand(self, expr): """Expands any environment variables and home shortcuts found in ``expr`` (like ``os.path.expanduser`` combined with ``os.path.expandvars``) :param expr: An expression containing environment variables (as ``$FOO``) or home shortcuts (as ``~/.bashrc``) :returns: The expanded string""" prev = os.environ os.environ = self.getdict() # noqa: B003 try: output = os.path.expanduser(os.path.expandvars(expr)) finally: os.environ = prev # noqa: B003 return output def expanduser(self, expr): """Expand home shortcuts (e.g., ``~/foo/bar`` or ``~john/foo/bar``) :param expr: An expression containing home shortcuts :returns: The expanded string""" prev = os.environ os.environ = self.getdict() # noqa: B003 try: output = os.path.expanduser(expr) finally: os.environ = prev # noqa: B003 return output # =================================================================================================== # Local Commands # =================================================================================================== class LocalCommand(ConcreteCommand): __slots__ = () QUOTE_LEVEL = 2 def __init__(self, executable, encoding="auto"): ConcreteCommand.__init__( self, executable, local.custom_encoding if encoding == "auto" else encoding ) @property def machine(self): return local def popen(self, args=(), cwd=None, env=None, **kwargs): if isinstance(args, str): args = (args,) return self.machine._popen( self.executable, self.formulate(0, args), cwd=self.cwd if cwd is None else cwd, env=self.env if env is None else env, **kwargs, ) # =================================================================================================== # Local Machine # =================================================================================================== class LocalMachine(BaseMachine): """The *local machine* (a singleton object). It serves as an entry point to everything related to the local machine, such as working directory and environment manipulation, command creation, etc. Attributes: * ``cwd`` - the local working directory * ``env`` - the local environment * ``custom_encoding`` - the local machine's default encoding (``sys.getfilesystemencoding()``) """ cwd = StaticProperty(LocalWorkdir) env = LocalEnv() custom_encoding = sys.getfilesystemencoding() uname = platform.uname()[0] _program_cache: Dict[Tuple[str, str], LocalPath] = {} def __init__(self): self._as_user_stack = [] if IS_WIN32: _EXTENSIONS = [ "", *env.get("PATHEXT", ":.exe:.bat").lower().split(os.path.pathsep), ] @classmethod def _which(cls, progname): progname = progname.lower() for p in cls.env.path: for ext in cls._EXTENSIONS: fn = p / (progname + ext) if fn.access("x") and not fn.is_dir(): return fn return None else: @classmethod def _which(cls, progname): for p in cls.env.path: fn = p / progname if fn.access("x") and not fn.is_dir(): return fn return None @classmethod def which(cls, progname): """Looks up a program in the ``PATH``. If the program is not found, raises :class:`CommandNotFound ` :param progname: The program's name. Note that if underscores (``_``) are present in the name, and the exact name is not found, they will be replaced in turn by hyphens (``-``) then periods (``.``), and the name will be looked up again for each alternative :returns: A :class:`LocalPath ` """ key = (progname, cls.env.get("PATH", "")) with contextlib.suppress(KeyError): return cls._program_cache[key] alternatives = [progname] if "_" in progname: alternatives += [progname.replace("_", "-"), progname.replace("_", ".")] for pn in alternatives: path = cls._which(pn) if path: cls._program_cache[key] = path return path raise CommandNotFound(progname, list(cls.env.path)) def path(self, *parts): """A factory for :class:`LocalPaths `. Usage: ``p = local.path("/usr", "lib", "python2.7")`` """ parts2 = [str(self.cwd)] for p in parts: if isinstance(p, RemotePath): raise TypeError(f"Cannot construct LocalPath from {p!r}") parts2.append(self.env.expanduser(str(p))) return LocalPath(os.path.join(*parts2)) def __contains__(self, cmd): try: self[cmd] except CommandNotFound: return False return True def __getitem__(self, cmd): """Returns a `Command` object representing the given program. ``cmd`` can be a string or a :class:`LocalPath `; if it is a path, a command representing this path will be returned; otherwise, the program name will be looked up in the system's ``PATH`` (using ``which``). Usage:: ls = local["ls"] """ if isinstance(cmd, LocalPath): 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)) # search for command return LocalCommand(self.which(cmd)) raise TypeError(f"cmd must not be a RemotePath: {cmd!r}") def _popen( self, executable, argv, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=None, env=None, new_session=False, **kwargs, ): if new_session: kwargs["start_new_session"] = True if IS_WIN32 and "startupinfo" not in kwargs and stdin not in (sys.stdin, None): # pylint: disable-next=used-before-assignment subsystem = get_pe_subsystem(str(executable)) # pylint: disable-next=used-before-assignment if subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI: # don't open a new console sui = subprocess.STARTUPINFO() kwargs["startupinfo"] = sui if hasattr(subprocess, "_subprocess"): sui.dwFlags |= ( subprocess._subprocess.STARTF_USESHOWWINDOW ) # @UndefinedVariable sui.wShowWindow = ( subprocess._subprocess.SW_HIDE ) # @UndefinedVariable else: sui.dwFlags |= subprocess.STARTF_USESHOWWINDOW # @UndefinedVariable sui.wShowWindow = subprocess.SW_HIDE # @UndefinedVariable if cwd is None: cwd = self.cwd envs = [self.env, env] env = {} for _env in envs: if not _env: continue if isinstance(_env, BaseEnv): _env = _env.getdict() env.update(_env) if self._as_user_stack: argv, executable = self._as_user_stack[-1](argv) logger.debug("Running %r", argv) proc = PlumbumLocalPopen( argv, executable=str(executable), stdin=stdin, stdout=stdout, stderr=stderr, cwd=str(cwd), env=env, **kwargs, ) # bufsize = 4096 proc._start_time = time.time() proc.custom_encoding = self.custom_encoding proc.argv = argv return proc def daemonic_popen(self, command, cwd="/", stdout=None, stderr=None, append=True): """ On POSIX systems: Run ``command`` as a UNIX daemon: fork a child process to setpid, redirect std handles to /dev/null, umask, close all fds, chdir to ``cwd``, then fork and exec ``command``. Returns a ``Popen`` process that can be used to poll/wait for the executed command (but keep in mind that you cannot access std handles) On Windows: Run ``command`` as a "Windows daemon": detach from controlling console and create a new process group. This means that the command will not receive console events and would survive its parent's termination. Returns a ``Popen`` object. .. note:: this does not run ``command`` as a system service, only detaches it from its parent. .. versionadded:: 1.3 """ if IS_WIN32: return win32_daemonize(command, cwd, stdout, stderr, append) return posix_daemonize(command, cwd, stdout, stderr, append) if IS_WIN32: def list_processes(self): # pylint: disable=no-self-use """ Returns information about all running processes (on Windows: using ``tasklist``) .. versionadded:: 1.3 """ import csv tasklist = local["tasklist"] output = tasklist("/V", "/FO", "CSV") lines = output.splitlines() rows = csv.reader(lines) try: header = next(rows) except StopIteration: raise RuntimeError("tasklist must at least have header") from None imgidx = header.index("Image Name") pididx = header.index("PID") statidx = header.index("Status") useridx = header.index("User Name") for row in rows: yield ProcInfo( int(row[pididx]), row[useridx], row[statidx], row[imgidx] ) else: def list_processes(self): """ Returns information about all running processes (on POSIX systems: using ``ps``) .. versionadded:: 1.3 """ ps = self["ps"] lines = ps("-e", "-o", "pid,uid,stat,args").splitlines() lines.pop(0) # header for line in lines: parts = line.strip().split() yield ProcInfo( int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:]) ) def pgrep(self, pattern): """ Process grep: return information about all processes whose command-line args match the given regex pattern """ pat = re.compile(pattern) for procinfo in self.list_processes(): if pat.search(procinfo.args): yield procinfo def session(self, new_session=False): """Creates a new :class:`ShellSession ` object; this invokes ``/bin/sh`` and executes commands on it over stdin/stdout/stderr""" return ShellSession(self["sh"].popen(new_session=new_session)) @contextmanager def tempdir(self): """A context manager that creates a temporary directory, which is removed when the context exits""" new_dir = self.path(mkdtemp()) try: yield new_dir finally: new_dir.delete() @contextmanager def as_user(self, username=None): """Run nested commands as the given user. For example:: head = local["head"] head("-n1", "/dev/sda1") # this will fail... with local.as_user(): head("-n1", "/dev/sda1") :param username: The user to run commands as. If not given, root (or Administrator) is assumed """ if IS_WIN32: if username is None: username = "Administrator" self._as_user_stack.append( lambda argv: ( [ "runas", "/savecred", f"/user:{username}", '"' + " ".join(str(a) for a in argv) + '"', ], self.which("runas"), ) ) else: if username is None: self._as_user_stack.append( lambda argv: (["sudo", *list(argv)], self.which("sudo")) ) else: self._as_user_stack.append( lambda argv: ( ["sudo", "-u", username, *list(argv)], self.which("sudo"), ) ) try: yield finally: self._as_user_stack.pop(-1) def as_root(self): """A shorthand for :func:`as_user("root") `""" return self.as_user() python = LocalCommand(sys.executable, custom_encoding) """A command that represents the current python interpreter (``sys.executable``)""" local = LocalMachine() """The *local machine* (a singleton object). It serves as an entry point to everything related to the local machine, such as working directory and environment manipulation, command creation, etc. Attributes: * ``cwd`` - the local working directory * ``env`` - the local environment * ``custom_encoding`` - the local machine's default encoding (``sys.getfilesystemencoding()``) """ plumbum-1.8.3/plumbum/machines/paramiko_machine.py0000644000000000000000000004232314613634536017262 0ustar00import contextlib import errno import logging import os import stat from plumbum.commands.base import shquote from plumbum.commands.processes import ProcessLineTimedOut, iter_lines from plumbum.machines.base import PopenAddons from plumbum.machines.remote import BaseRemoteMachine from plumbum.machines.session import ShellSession from plumbum.path.local import LocalPath from plumbum.path.remote import RemotePath, StatRes try: # Sigh... we need to gracefully-import paramiko for Sphinx builds, etc import paramiko except ImportError: class paramiko: # type: ignore[no-redef] def __bool__(self): return False def __getattr__(self, name): raise ImportError("No module named paramiko") paramiko = paramiko() # type: ignore[operator] logger = logging.getLogger("plumbum.paramiko") class ParamikoPopen(PopenAddons): def __init__( self, argv, stdin, stdout, stderr, encoding, stdin_file=None, stdout_file=None, stderr_file=None, ): self.argv = argv self.channel = stdout.channel self.stdin = stdin self.stdout = stdout self.stderr = stderr self.custom_encoding = encoding self.returncode = None self.pid = None self.stdin_file = stdin_file self.stdout_file = stdout_file self.stderr_file = stderr_file def poll(self): if self.returncode is None and self.channel.exit_status_ready(): return self.wait() return self.returncode def wait(self): if self.returncode is None: self.channel.recv_exit_status() self.returncode = self.channel.exit_status self.close() return self.returncode def close(self): self.channel.shutdown_read() self.channel.shutdown_write() self.channel.close() @staticmethod def kill(): # possible way to obtain pid: # "(cmd ; echo $?) & echo ?!" # and then client.exec_command("kill -9 %s" % (pid,)) raise OSError("Cannot kill remote processes, we don't have their PIDs") terminate = kill def send_signal(self, sig): raise NotImplementedError() def communicate(self): stdout = [] stderr = [] infile = self.stdin_file sources = [ ("1", stdout, self.stdout, self.stdout_file), ("2", stderr, self.stderr, self.stderr_file), ] i = 0 while sources: if infile: try: line = infile.readline() except (ValueError, OSError): line = None logger.debug("communicate: %r", line) if not line: infile.close() infile = None self.stdin.close() else: self.stdin.write(line) self.stdin.flush() i = (i + 1) % len(sources) _name, coll, pipe, outfile = sources[i] line = pipe.readline() # logger.debug("%s> %r", name, line) if not line: del sources[i] elif outfile: outfile.write(line) outfile.flush() else: coll.append(line) self.wait() stdout = "".join(s for s in stdout).encode(self.custom_encoding) stderr = "".join(s for s in stderr).encode(self.custom_encoding) return stdout, stderr def iter_lines(self, timeout=None, **kwargs): if timeout is not None: raise NotImplementedError( "The 'timeout' parameter is not supported with ParamikoMachine" ) return iter_lines(self, _iter_lines=_iter_lines, **kwargs) __iter__ = iter_lines class ParamikoMachine(BaseRemoteMachine): """ An implementation of :class:`remote machine ` over Paramiko (a Python implementation of openSSH2 client/server). Invoking a remote command translates to invoking it over SSH :: with ParamikoMachine("yourhostname") as rem: r_ls = rem["ls"] # r_ls is the remote `ls` # executing r_ls() is equivalent to `ssh yourhostname ls`, only without # spawning a new ssh client :param host: the host name to connect to (SSH server) :param user: the user to connect as (if ``None``, the default will be used) :param port: the server's port (if ``None``, the default will be used) :param password: the user's password (if a password-based authentication is to be performed) (if ``None``, key-based authentication will be used) :param keyfile: the path to the identity file (if ``None``, the default will be used) :param load_system_host_keys: whether or not to load the system's host keys (from ``/etc/ssh`` and ``~/.ssh``). The default is ``True``, which means Paramiko behaves much like the ``ssh`` command-line client :param missing_host_policy: the value passed to the underlying ``set_missing_host_key_policy`` of the client. The default is ``None``, which means ``set_missing_host_key_policy`` is not invoked and paramiko's default behavior (reject) is employed :param encoding: the remote machine's encoding (defaults to UTF8) :param look_for_keys: set to False to disable searching for discoverable private key files in ``~/.ssh`` :param connect_timeout: timeout for TCP connection .. note:: If Paramiko 1.15 or above is installed, can use GSS_API authentication :param bool gss_auth: ``True`` if you want to use GSS-API authentication :param bool gss_kex: Perform GSS-API Key Exchange and user authentication :param bool gss_deleg_creds: Delegate GSS-API client credentials or not :param str gss_host: The targets name in the kerberos database. default: hostname :param bool get_pty: Execute remote commands with allocated pseudo-tty. default: False :param bool load_system_ssh_config: read system SSH config for ProxyCommand configuration. default: False """ class RemoteCommand(BaseRemoteMachine.RemoteCommand): # type: ignore[valid-type, misc] def __or__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __gt__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __rshift__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __ge__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __lt__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __lshift__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __init__( self, host, user=None, port=None, password=None, keyfile=None, load_system_host_keys=True, missing_host_policy=None, encoding="utf8", look_for_keys=None, connect_timeout=None, keep_alive=0, gss_auth=False, gss_kex=None, gss_deleg_creds=None, gss_host=None, get_pty=False, load_system_ssh_config=False, ): self.host = host kwargs = {} if user: self._fqhost = f"{user}@{host}" kwargs["username"] = user else: self._fqhost = host self._client = paramiko.SSHClient() if load_system_host_keys: self._client.load_system_host_keys() if port is not None: kwargs["port"] = port if keyfile is not None: kwargs["key_filename"] = keyfile if password is not None: kwargs["password"] = password if missing_host_policy is not None: self._client.set_missing_host_key_policy(missing_host_policy) if look_for_keys is not None: kwargs["look_for_keys"] = look_for_keys if connect_timeout is not None: kwargs["timeout"] = connect_timeout if gss_auth: kwargs["gss_auth"] = gss_auth kwargs["gss_kex"] = gss_kex kwargs["gss_deleg_creds"] = gss_deleg_creds if not gss_host: gss_host = host kwargs["gss_host"] = gss_host if load_system_ssh_config: ssh_config = paramiko.SSHConfig() with open(os.path.expanduser("~/.ssh/config"), encoding="utf-8") as f: ssh_config.parse(f) with contextlib.suppress(KeyError): hostConfig = ssh_config.lookup(host) kwargs["sock"] = paramiko.ProxyCommand(hostConfig["proxycommand"]) self._client.connect(host, **kwargs) self._keep_alive = keep_alive self._sftp = None self._get_pty = get_pty BaseRemoteMachine.__init__(self, encoding, connect_timeout) def __str__(self): return f"paramiko://{self._fqhost}" def close(self): BaseRemoteMachine.close(self) self._client.close() @property def sftp(self): """ Returns an SFTP client on top of the current SSH connection; it can be used to manipulate files directly, much like an interactive FTP/SFTP session """ if not self._sftp: self._sftp = self._client.open_sftp() return self._sftp def session( 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() trans.set_keepalive(self._keep_alive) chan = trans.open_session() if isatty: chan.get_pty(term, width, height) chan.set_combine_stderr(True) chan.invoke_shell() stdin = chan.makefile("wb", -1) stdout = chan.makefile("rb", -1) stderr = chan.makefile_stderr("rb", -1) proc = ParamikoPopen([""], stdin, stdout, stderr, self.custom_encoding) return ShellSession(proc, self.custom_encoding, isatty) def popen( self, args, stdin=None, stdout=None, stderr=None, new_session=False, # noqa: ARG002 env=None, cwd=None, ): # new_session is ignored for ParamikoMachine argv = [] envdelta = self.env.getdelta() if env: envdelta.update(env) argv.extend(["cd", str(cwd or self.cwd), "&&"]) if envdelta: argv.append("env") argv.extend(f"{k}={shquote(v)}" for k, v in envdelta.items()) argv.extend(args.formulate()) cmdline = " ".join(argv) logger.debug(cmdline) si, so, se = self._client.exec_command(cmdline, 1, get_pty=self._get_pty) return ParamikoPopen( argv, si, so, se, self.custom_encoding, stdin_file=stdin, stdout_file=stdout, stderr_file=stderr, ) def download(self, src, dst): if isinstance(src, LocalPath): raise TypeError(f"src of download cannot be {src!r}") if isinstance(src, RemotePath) and src.remote != self: raise TypeError(f"src {src!r} points to a different remote machine") if isinstance(dst, RemotePath): raise TypeError(f"dst of download cannot be {dst!r}") return self._download( src if isinstance(src, RemotePath) else self.path(src), dst if isinstance(dst, LocalPath) else LocalPath(dst), ) def _download(self, src, dst): if src.is_dir(): if not dst.exists(): self.sftp.mkdir(str(dst)) for fn in src: self._download(fn, dst / fn.name) elif dst.is_dir(): self.sftp.get(str(src), str(dst / src.name)) else: self.sftp.get(str(src), str(dst)) def upload(self, src, dst): if isinstance(src, RemotePath): raise TypeError(f"src of upload cannot be {src!r}") if isinstance(dst, LocalPath): raise TypeError(f"dst of upload cannot be {dst!r}") if isinstance(dst, RemotePath) and dst.remote != self: raise TypeError(f"dst {dst!r} points to a different remote machine") return self._upload( src if isinstance(src, LocalPath) else LocalPath(src), dst if isinstance(dst, RemotePath) else self.path(dst), ) def _upload(self, src, dst): if src.is_dir(): if not dst.exists(): self.sftp.mkdir(str(dst)) for fn in src: self._upload(fn, dst / fn.name) elif dst.is_dir(): self.sftp.put(str(src), str(dst / src.name)) else: self.sftp.put(str(src), str(dst)) def connect_sock(self, dport, dhost="localhost", ipv6=False): """Returns a Paramiko ``Channel``, connected to dhost:dport on the remote machine. The ``Channel`` behaves like a regular socket; you can ``send`` and ``recv`` on it and the data will pass encrypted over SSH. Usage:: mach = ParamikoMachine("myhost") sock = mach.connect_sock(12345) data = sock.recv(100) sock.send("foobar") sock.close() """ if ipv6 and dhost == "localhost": dhost = "::1" srcaddr = ("::1", 0, 0, 0) if ipv6 else ("127.0.0.1", 0) trans = self._client.get_transport() trans.set_keepalive(self._keep_alive) chan = trans.open_channel("direct-tcpip", (dhost, dport), srcaddr) return SocketCompatibleChannel(chan) # # Path implementation # def _path_listdir(self, fn): return self.sftp.listdir(str(fn)) def _path_read(self, fn): f = self.sftp.open(str(fn), "rb") data = f.read() f.close() return data def _path_write(self, fn, data): if self.custom_encoding and isinstance(data, str): data = data.encode(self.custom_encoding) f = self.sftp.open(str(fn), "wb") f.write(data) f.close() def _path_stat(self, fn): try: st = self.sftp.stat(str(fn)) except OSError as e: if e.errno == errno.ENOENT: return None raise res = StatRes( ( st.st_mode, 0, 0, 0, st.st_uid, st.st_gid, st.st_size, st.st_atime, st.st_mtime, 0, ) ) if stat.S_ISDIR(st.st_mode): res.text_mode = "directory" if stat.S_ISREG(st.st_mode): res.text_mode = "regular file" return res def daemonic_popen(self, command, cwd="/", stdout=None, stderr=None, append=True): raise NotImplementedError("This is not implemented on ParamikoMachine!") ################################################################################################### # Make paramiko.Channel adhere to the socket protocol, namely, send and recv should fail # when the socket has been closed ################################################################################################### class SocketCompatibleChannel: def __init__(self, chan): self._chan = chan def __getattr__(self, name): return getattr(self._chan, name) def send(self, s): if self._chan.closed: raise OSError(errno.EBADF, "Bad file descriptor") return self._chan.send(s) def recv(self, count): if self._chan.closed: raise OSError(errno.EBADF, "Bad file descriptor") return self._chan.recv(count) ################################################################################################### # Custom iter_lines for paramiko.Channel ################################################################################################### def _iter_lines( proc, decode, # noqa: ARG001 linesize, line_timeout=None, ): from selectors import EVENT_READ, DefaultSelector # Python 3.4+ implementation def selector(): sel = DefaultSelector() sel.register(proc.stdout.channel, EVENT_READ) while True: ready = sel.select(line_timeout) if not ready and line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) for _key, _mask in ready: yield for _ in selector(): if proc.stdout.channel.recv_ready(): yield 0, proc.stdout.readline(linesize) if proc.stdout.channel.recv_stderr_ready(): yield 1, proc.stderr.readline(linesize) if proc.poll() is not None: break for line in proc.stdout: yield 0, line for line in proc.stderr: yield 1, line plumbum-1.8.3/plumbum/machines/remote.py0000644000000000000000000003710514613634536015270 0ustar00import contextlib import re from tempfile import NamedTemporaryFile from plumbum.commands import CommandNotFound, ConcreteCommand, shquote from plumbum.lib import ProcInfo from plumbum.machines.base import BaseMachine from plumbum.machines.env import BaseEnv from plumbum.path.local import LocalPath from plumbum.path.remote import RemotePath, RemoteWorkdir, StatRes class RemoteEnv(BaseEnv): """The remote machine's environment; exposes a dict-like interface""" __slots__ = ["_orig", "remote"] def __init__(self, remote): session = remote._session # GNU env has a -0 argument; use it if present. Otherwise, # fall back to calling printenv on each (possible) variable # from plain env. env0 = session.run("env -0; echo") if env0[0] == 0 and not env0[2].rstrip(): _curr = dict( line.split("=", 1) for line in env0[1].split("\x00") if "=" in line ) else: lines = session.run("env; echo")[1].splitlines() split = (line.split("=", 1) for line in lines) keys = (line[0] for line in split if len(line) > 1) runs = ((key, session.run(f'printenv "{key}"; echo')) for key in keys) _curr = { key: run[1].rstrip("\n") for (key, run) in runs if run[0] == 0 and run[1].rstrip("\n") and not run[2] } super().__init__(remote.path, ":", _curr=_curr) self.remote = remote self._orig = self._curr.copy() def __delitem__(self, name): BaseEnv.__delitem__(self, name) self.remote._session.run(f"unset {name}") def __setitem__(self, name, value): BaseEnv.__setitem__(self, name, value) self.remote._session.run(f"export {name}={shquote(value)}") def pop(self, name, *default): BaseEnv.pop(self, name, *default) self.remote._session.run(f"unset {name}") def update(self, *args, **kwargs): BaseEnv.update(self, *args, **kwargs) self.remote._session.run( "export " + " ".join(f"{k}={shquote(v)}" for k, v in self.getdict().items()) ) def expand(self, expr): """Expands any environment variables and home shortcuts found in ``expr`` (like ``os.path.expanduser`` combined with ``os.path.expandvars``) :param expr: An expression containing environment variables (as ``$FOO``) or home shortcuts (as ``~/.bashrc``) :returns: The expanded string""" return self.remote.expand(expr) def expanduser(self, expr): """Expand home shortcuts (e.g., ``~/foo/bar`` or ``~john/foo/bar``) :param expr: An expression containing home shortcuts :returns: The expanded string""" return self.remote.expanduser(expr) # def clear(self): # BaseEnv.clear(self, *args, **kwargs) # self.remote._session.run("export %s" % " ".join("%s=%s" % (k, v) for k, v in self.getdict())) def getdelta(self): """Returns the difference between the this environment and the original environment of the remote machine""" self._curr["PATH"] = self.path.join() delta = {} for k, v in self._curr.items(): if k not in self._orig: delta[k] = str(v) for k, v in self._orig.items(): if k not in self._curr: delta[k] = "" else: if v != self._curr[k]: delta[k] = self._curr[k] return delta class RemoteCommand(ConcreteCommand): __slots__ = ("remote",) QUOTE_LEVEL = 1 def __init__(self, remote, executable, encoding="auto"): self.remote = remote ConcreteCommand.__init__( self, executable, remote.custom_encoding if encoding == "auto" else encoding ) @property def machine(self): return self.remote def __repr__(self): return f"RemoteCommand({self.remote!r}, {self.executable!r})" def popen(self, args=(), **kwargs): return self.remote.popen(self[args], **kwargs) def nohup(self, cwd=".", stdout="nohup.out", stderr=None, append=True): """Runs a command detached.""" return self.machine.daemonic_popen(self, cwd, stdout, stderr, append) class ClosedRemoteMachine(Exception): pass class ClosedRemote: __slots__ = ["_obj", "__weakref__"] def __init__(self, obj): self._obj = obj def close(self): pass def __getattr__(self, name): raise ClosedRemoteMachine(f"{self._obj!r} has been closed") class BaseRemoteMachine(BaseMachine): """Represents a *remote machine*; serves as an entry point to everything related to that remote machine, such as working directory and environment manipulation, command creation, etc. Attributes: * ``cwd`` - the remote working directory * ``env`` - the remote environment * ``custom_encoding`` - the remote machine's default encoding (assumed to be UTF8) * ``connect_timeout`` - the connection timeout There also is a _cwd attribute that exists if the cwd is not current (del if cwd is changed). """ # allow inheritors to override the RemoteCommand class RemoteCommand = RemoteCommand @property def cwd(self): if not hasattr(self, "_cwd"): self._cwd = RemoteWorkdir(self) return self._cwd def __init__(self, encoding="utf8", connect_timeout=10, new_session=False): self.custom_encoding = encoding self.connect_timeout = connect_timeout self._session = self.session(new_session=new_session) self.uname = self._get_uname() self.env = RemoteEnv(self) self._python = None self._program_cache = {} def _get_uname(self): rc, out, _ = self._session.run("uname", retcode=None) if rc == 0: return out.strip() rc, out, _ = self._session.run( "python3 -c 'import platform;print(platform.uname()[0])'", retcode=None ) if rc == 0: return out.strip() # all POSIX systems should have uname. make an educated guess it's Windows return "Windows" def __repr__(self): return f"<{self.__class__.__name__} {self}>" def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): """closes the connection to the remote machine; all paths and programs will become defunct""" self._session.close() self._session = ClosedRemote(self) def path(self, *parts): """A factory for :class:`RemotePaths `. Usage: ``p = rem.path("/usr", "lib", "python2.7")`` """ parts2 = [str(self.cwd)] for p in parts: if isinstance(p, LocalPath): raise TypeError(f"Cannot construct RemotePath from {p!r}") parts2.append(self.expanduser(str(p))) return RemotePath(self, *parts2) def which(self, progname): """Looks up a program in the ``PATH``. If the program is not found, raises :class:`CommandNotFound ` :param progname: The program's name. Note that if underscores (``_``) are present in the name, and the exact name is not found, they will be replaced in turn by hyphens (``-``) then periods (``.``), and the name will be looked up again for each alternative :returns: A :class:`RemotePath ` """ key = (progname, self.env.get("PATH", "")) with contextlib.suppress(KeyError): return self._program_cache[key] alternatives = [progname] if "_" in progname: alternatives += [progname.replace("_", "-"), progname.replace("_", ".")] for name in alternatives: for p in self.env.path: fn = p / name if fn.access("x") and not fn.is_dir(): self._program_cache[key] = fn return fn raise CommandNotFound(progname, self.env.path) def __getitem__(self, cmd): """Returns a `Command` object representing the given program. ``cmd`` can be a string or a :class:`RemotePath `; if it is a path, a command representing this path will be returned; otherwise, the program name will be looked up in the system's ``PATH`` (using ``which``). Usage:: r_ls = rem["ls"] """ if isinstance(cmd, RemotePath): if cmd.remote is self: return self.RemoteCommand(self, cmd) raise TypeError( f"Given path does not belong to this remote machine: {cmd!r}" ) if not isinstance(cmd, LocalPath): return self.RemoteCommand( self, self.path(cmd) if "/" in cmd or "\\" in cmd else self.which(cmd) ) raise TypeError(f"cmd must not be a LocalPath: {cmd!r}") @property def python(self): """A command that represents the default remote python interpreter""" if not self._python: self._python = self["python3"] return self._python def session(self, isatty=False, *, new_session=False): """Creates a new :class:`ShellSession ` object; this invokes the user's shell on the remote machine and executes commands on it over stdin/stdout/stderr """ raise NotImplementedError() def download(self, src, dst): """Downloads a remote file/directory (``src``) to a local destination (``dst``). ``src`` must be a string or a :class:`RemotePath ` pointing to this remote machine, and ``dst`` must be a string or a :class:`LocalPath `""" raise NotImplementedError() def upload(self, src, dst): """Uploads a local file/directory (``src``) to a remote destination (``dst``). ``src`` must be a string or a :class:`LocalPath `, and ``dst`` must be a string or a :class:`RemotePath ` pointing to this remote machine""" raise NotImplementedError() def popen(self, args, **kwargs): """Spawns the given command on the remote machine, returning a ``Popen``-like object; do not use this method directly, unless you need "low-level" control on the remote process""" raise NotImplementedError() def list_processes(self): """ Returns information about all running processes (on POSIX systems: using ``ps``) .. versionadded:: 1.3 """ ps = self["ps"] lines = ps("-e", "-o", "pid,uid,stat,args").splitlines() lines.pop(0) # header for line in lines: parts = line.strip().split() yield ProcInfo(int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:])) def pgrep(self, pattern): """ Process grep: return information about all processes whose command-line args match the given regex pattern """ pat = re.compile(pattern) for procinfo in self.list_processes(): if pat.search(procinfo.args): yield procinfo @contextlib.contextmanager def tempdir(self): """A context manager that creates a remote temporary directory, which is removed when the context exits""" _, out, _ = self._session.run( "mktemp -d 2>/dev/null || mktemp -d tmp.XXXXXXXXXX" ) local_dir = self.path(out.strip()) try: yield local_dir finally: local_dir.delete() # # Path implementation # def _path_listdir(self, fn): files = self._session.run(f"ls -a {shquote(fn)}")[1].splitlines() files.remove(".") files.remove("..") return files def _path_glob(self, fn, pattern): # shquote does not work here due to the way bash loops use space as a separator pattern = pattern.replace(" ", r"\ ") fn = fn.replace(" ", r"\ ") matches = self._session.run(rf"for fn in {fn}/{pattern}; do echo $fn; done")[ 1 ].splitlines() if len(matches) == 1 and not self._path_stat(matches[0]): return [] # pattern expansion failed return matches def _path_getuid(self, fn): stat_cmd = ( "stat -c '%u,%U' " if self.uname not in ("Darwin", "FreeBSD") else "stat -f '%u,%Su' " ) return self._session.run(stat_cmd + shquote(fn))[1].strip().split(",") def _path_getgid(self, fn): stat_cmd = ( "stat -c '%g,%G' " if self.uname not in ("Darwin", "FreeBSD") else "stat -f '%g,%Sg' " ) return self._session.run(stat_cmd + shquote(fn))[1].strip().split(",") def _path_stat(self, fn): if self.uname not in ("Darwin", "FreeBSD"): stat_cmd = "stat -c '%F,%f,%i,%d,%h,%u,%g,%s,%X,%Y,%Z' " else: stat_cmd = "stat -f '%HT,%Xp,%i,%d,%l,%u,%g,%z,%a,%m,%c' " rc, out, _ = self._session.run(stat_cmd + shquote(fn), retcode=None) if rc != 0: 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.text_mode = text_mode return res def _path_delete(self, fn): self._session.run(f"rm -rf {shquote(fn)}") def _path_move(self, src, dst): self._session.run(f"mv {shquote(src)} {shquote(dst)}") def _path_copy(self, src, dst): self._session.run(f"cp -r {shquote(src)} {shquote(dst)}") def _path_mkdir( self, fn, mode=None, # noqa: ARG002 minus_p=True, ): p_str = "-p " if minus_p else "" cmd = f"mkdir {p_str}{shquote(fn)}" self._session.run(cmd) def _path_chmod(self, mode, fn): self._session.run(f"chmod {mode:o} {shquote(fn)}") def _path_touch(self, path): self._session.run(f"touch {path}") def _path_chown(self, fn, owner, group, recursive): args = ["chown"] if recursive: args.append("-R") if owner is not None and group is not None: args.append(f"{owner}:{group}") elif owner is not None: args.append(str(owner)) elif group is not None: args.append(f":{group}") args.append(shquote(fn)) self._session.run(" ".join(args)) def _path_read(self, fn): data = self["cat"](fn) if self.custom_encoding and isinstance(data, str): return data.encode(self.custom_encoding) return data def _path_write(self, fn, data): if self.custom_encoding and isinstance(data, str): data = data.encode(self.custom_encoding) with NamedTemporaryFile() as f: f.write(data) f.flush() f.seek(0) self.upload(f.name, fn) def _path_link(self, src, dst, symlink): symlink_str = "-s " if symlink else "" self._session.run(f"ln {symlink_str}{shquote(src)} {shquote(dst)}") def expand(self, expr): return self._session.run(f"echo {expr}")[1].strip() def expanduser(self, expr): if not any(part.startswith("~") for part in expr.split("/")): return expr # we escape all $ signs to avoid expanding env-vars expr_repl = expr.replace("$", "\\$") return self._session.run(f"echo {expr_repl}")[1].strip() plumbum-1.8.3/plumbum/machines/session.py0000644000000000000000000002704114613634536015456 0ustar00import contextlib import logging import random import threading import time from plumbum.commands import BaseCommand, run_proc from plumbum.commands.processes import ProcessExecutionError from plumbum.machines.base import PopenAddons class ShellSessionError(Exception): """Raises when something goes wrong when calling :func:`ShellSession.popen `""" class SSHCommsError(ProcessExecutionError, EOFError): """Raises when the communication channel can't be created on the remote host or it times out.""" class SSHCommsChannel2Error(SSHCommsError): """Raises when channel 2 (stderr) is not available""" class IncorrectLogin(SSHCommsError): """Raises when incorrect login credentials are provided""" class HostPublicKeyUnknown(SSHCommsError): """Raises when the host public key isn't known""" shell_logger = logging.getLogger("plumbum.shell") # =================================================================================================== # Shell Session Popen # =================================================================================================== class MarkedPipe: """A pipe-like object from which you can read lines; the pipe will return report EOF (the empty string) when a special marker is detected""" __slots__ = ["pipe", "marker", "__weakref__"] def __init__(self, pipe, marker): self.pipe = pipe self.marker = marker self.marker = bytes(self.marker, "ascii") def close(self): """'Closes' the marked pipe; following calls to ``readline`` will return "" """ # consume everything while self.readline(): pass self.pipe = None def readline(self): """Reads the next line from the pipe; returns "" when the special marker is reached. Raises ``EOFError`` if the underlying pipe has closed""" if self.pipe is None: return b"" line = self.pipe.readline() if not line: raise EOFError() if line.strip() == self.marker: self.pipe = None return b"" return line class SessionPopen(PopenAddons): """A shell-session-based ``Popen``-like object (has the following attributes: ``stdin``, ``stdout``, ``stderr``, ``returncode``)""" def __init__(self, proc, argv, isatty, stdin, stdout, stderr, encoding, *, host): self.host = host self.proc = proc self.argv = argv self.isatty = isatty self.stdin = stdin self.stdout = stdout self.stderr = stderr self.custom_encoding = encoding self.returncode = None self._done = False def poll(self): """Returns the process' exit code or ``None`` if it's still running""" return self.returncode if self._done else None def wait(self): """Waits for the process to terminate and returns its exit code""" self.communicate() return self.returncode def communicate(self, input=None): # pylint: disable=redefined-builtin """Consumes the process' stdout and stderr until the it terminates. :param input: An optional bytes/buffer object to send to the process over stdin :returns: A tuple of (stdout, stderr) """ stdout = [] stderr = [] sources = [("1", stdout, self.stdout)] if not self.isatty: # in tty mode, stdout and stderr are unified sources.append(("2", stderr, self.stderr)) i = 0 while sources: if input: chunk = input[:1000] self.stdin.write(chunk) self.stdin.flush() input = input[1000:] i = (i + 1) % len(sources) name, coll, pipe = sources[i] try: line = pipe.readline() shell_logger.debug("%s> %r", name, line) except EOFError as err: shell_logger.debug("%s> Nothing returned.", name) self.proc.poll() returncode = self.proc.returncode stdout = b"".join(stdout).decode(self.custom_encoding, "ignore") stderr = b"".join(stderr).decode(self.custom_encoding, "ignore") argv = self.argv.decode(self.custom_encoding, "ignore").split(";")[:1] if returncode == 5: raise IncorrectLogin( argv, returncode, stdout, stderr, message="Incorrect username or password provided", host=self.host, ) from None if returncode == 6: raise HostPublicKeyUnknown( argv, returncode, stdout, stderr, message="The authenticity of the host can't be established", host=self.host, ) from None if returncode != 0: raise SSHCommsError( argv, returncode, stdout, stderr, message="SSH communication failed", host=self.host, ) from None if name == "2": raise SSHCommsChannel2Error( argv, returncode, stdout, stderr, message="No stderr result detected. Does the remote have Bash as the default shell?", host=self.host, ) from None raise SSHCommsError( argv, returncode, stdout, stderr, message="No communication channel detected. Does the remote exist?", host=self.host, ) from err if not line: del sources[i] else: coll.append(line) if self.isatty: stdout.pop(0) # discard first line of prompt try: self.returncode = int(stdout.pop(-1)) except (IndexError, ValueError): self.returncode = "Unknown" self._done = True stdout = b"".join(stdout) stderr = b"".join(stderr) return stdout, stderr class ShellSession: """An abstraction layer over *shell sessions*. A shell session is the execution of an interactive shell (``/bin/sh`` or something compatible), over which you may run commands (sent over stdin). The output of is then read from stdout and stderr. Shell sessions are less "robust" than executing a process on its own, and they are susseptible to all sorts of malformatted-strings attacks, and there is little benefit from using them locally. However, they can greatly speed up remote connections, and are required for the implementation of :class:`SshMachine `, as they allow us to send multiple commands over a single SSH connection (setting up separate SSH connections incurs a high overhead). Try to avoid using shell sessions, unless you know what you're doing. Instances of this class may be used as *context-managers*. :param proc: The underlying shell process (with open stdin, stdout and stderr) :param encoding: The encoding to use for the shell session. If ``"auto"``, the underlying process' encoding is used. :param isatty: If true, assume the shell has a TTY and that stdout and stderr are unified :param connect_timeout: The timeout to connect to the shell, after which, if no prompt is seen, the shell process is killed """ def __init__( self, proc, encoding="auto", isatty=False, connect_timeout=5, *, host=None ): self.host = host self.proc = proc self.custom_encoding = proc.custom_encoding if encoding == "auto" else encoding self.isatty = isatty self._lock = threading.RLock() self._current = None self._startup_result = None if connect_timeout: def closer(): shell_logger.error( "Connection to %s timed out (%d sec)", proc, connect_timeout ) self.close() timer = threading.Timer(connect_timeout, closer) timer.start() try: self._startup_result = self.run("") finally: if connect_timeout: timer.cancel() def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def __del__(self): with contextlib.suppress(Exception): self.close() def alive(self): """Returns ``True`` if the underlying shell process is alive, ``False`` otherwise""" return self.proc and self.proc.poll() is None def close(self): """Closes (terminates) the shell session""" if not self.alive(): return with contextlib.suppress(ValueError, OSError): self.proc.stdin.write(b"\nexit\n\n\nexit\n\n") self.proc.stdin.flush() time.sleep(0.05) for p in (self.proc.stdin, self.proc.stdout, self.proc.stderr): with contextlib.suppress(Exception): p.close() with contextlib.suppress(OSError): self.proc.kill() self.proc = None def popen(self, cmd): """Runs the given command in the shell, adding some decoration around it. Only a single command can be executed at any given time. :param cmd: The command (string or :class:`Command ` object) to run :returns: A :class:`SessionPopen ` instance """ if self.proc is None: raise ShellSessionError("Shell session has already been closed") if self._current and not self._current._done: raise ShellSessionError("Each shell may start only one process at a time") full_cmd = cmd.formulate(1) if isinstance(cmd, BaseCommand) else cmd marker = f"--.END{time.time() * random.random()}.--" if full_cmd.strip(): full_cmd += " ; " else: full_cmd = "true ; " full_cmd += f"echo $? ; echo '{marker}'" if not self.isatty: full_cmd += f" ; echo '{marker}' 1>&2" if self.custom_encoding: full_cmd = full_cmd.encode(self.custom_encoding) shell_logger.debug("Running %r", full_cmd) self.proc.stdin.write(full_cmd + b"\n") self.proc.stdin.flush() self._current = SessionPopen( self.proc, full_cmd, self.isatty, self.proc.stdin, MarkedPipe(self.proc.stdout, marker), MarkedPipe(self.proc.stderr, marker), self.custom_encoding, host=self.host, ) return self._current def run(self, cmd, retcode=0): """Runs the given command :param cmd: The command (string or :class:`Command ` object) to run :param retcode: The expected return code (0 by default). Set to ``None`` in order to ignore erroneous return codes :returns: A tuple of (return code, stdout, stderr) """ with self._lock: return run_proc(self.popen(cmd), retcode) plumbum-1.8.3/plumbum/machines/ssh_machine.py0000644000000000000000000003545514613634536016264 0ustar00import re import socket import warnings from contextlib import closing from plumbum.commands import ProcessExecutionError, shquote from plumbum.lib import IS_WIN32 from plumbum.machines.local import local from plumbum.machines.remote import BaseRemoteMachine from plumbum.machines.session import ShellSession from plumbum.path.local import LocalPath from plumbum.path.remote import RemotePath def _get_free_port(): """Attempts to find a free port.""" s = socket.socket() with closing(s): s.bind(("localhost", 0)) return s.getsockname()[1] class SshTunnel: """An object representing an SSH tunnel (created by :func:`SshMachine.tunnel `)""" __slots__ = ["_session", "_lport", "_dport", "_reverse", "__weakref__"] def __init__(self, session, lport, dport, reverse): self._session = session self._lport = lport self._dport = dport self._reverse = reverse if reverse and str(dport) == "0" and session._startup_result is not None: # Try to detect assigned remote port. regex = re.compile( r"^Allocated port (\d+) for remote forward to .+$", re.MULTILINE ) match = regex.search(session._startup_result[2]) if match: self._dport = match.group(1) def __repr__(self): tunnel = self._session.proc if self._session.alive() else "(defunct)" return f"" def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): """Closes(terminates) the tunnel""" self._session.close() @property def lport(self): """Tunneled port or socket on the local machine.""" return self._lport @property def dport(self): """Tunneled port or socket on the remote machine.""" return self._dport @property def reverse(self): """Represents if the tunnel is a reverse tunnel.""" return self._reverse class SshMachine(BaseRemoteMachine): """ An implementation of :class:`remote machine ` over SSH. Invoking a remote command translates to invoking it over SSH :: with SshMachine("yourhostname") as rem: r_ls = rem["ls"] # r_ls is the remote `ls` # executing r_ls() translates to `ssh yourhostname ls` :param host: the host name to connect to (SSH server) :param user: the user to connect as (if ``None``, the default will be used) :param port: the server's port (if ``None``, the default will be used) :param keyfile: the path to the identity file (if ``None``, the default will be used) :param ssh_command: the ``ssh`` command to use; this has to be a ``Command`` object; if ``None``, the default ssh client will be used. :param scp_command: the ``scp`` command to use; this has to be a ``Command`` object; if ``None``, the default scp program will be used. :param ssh_opts: any additional options for ``ssh`` (a list of strings) :param scp_opts: any additional options for ``scp`` (a list of strings) :param password: the password to use; requires ``sshpass`` be installed. Cannot be used in conjunction with ``ssh_command`` or ``scp_command`` (will be ignored). NOTE: THIS IS A SECURITY RISK! :param encoding: the remote machine's encoding (defaults to UTF8) :param connect_timeout: specify a connection timeout (the time until shell prompt is seen). The default is 10 seconds. Set to ``None`` to disable :param new_session: whether or not to start the background session as a new session leader (setsid). This will prevent it from being killed on Ctrl+C (SIGINT) """ def __init__( self, host, user=None, port=None, keyfile=None, ssh_command=None, scp_command=None, ssh_opts=(), scp_opts=(), password=None, encoding="utf8", connect_timeout=10, new_session=False, ): if ssh_command is None: if password is not None: ssh_command = local["sshpass"]["-p", password, "ssh"] else: ssh_command = local["ssh"] if scp_command is None: if password is not None: scp_command = local["sshpass"]["-p", password, "scp"] else: scp_command = local["scp"] scp_args = [] ssh_args = [] self.host = host if user: self._fqhost = f"{user}@{host}" else: self._fqhost = host if port: ssh_args.extend(["-p", str(port)]) scp_args.extend(["-P", str(port)]) if keyfile: ssh_args.extend(["-i", str(keyfile)]) scp_args.extend(["-i", str(keyfile)]) scp_args.append("-r") ssh_args.extend(ssh_opts) scp_args.extend(scp_opts) self._ssh_command = ssh_command[tuple(ssh_args)] self._scp_command = scp_command[tuple(scp_args)] BaseRemoteMachine.__init__( self, encoding=encoding, connect_timeout=connect_timeout, new_session=new_session, ) def __str__(self): return f"ssh://{self._fqhost}" def popen(self, args, ssh_opts=(), env=None, cwd=None, **kwargs): cmdline = [] cmdline.extend(ssh_opts) cmdline.append(self._fqhost) if args: envdelta = {} if hasattr(self, "env"): envdelta.update(self.env.getdelta()) if env: envdelta.update(env) if cwd is None: cwd = getattr(self, "cwd", None) if cwd: cmdline.extend(["cd", str(cwd), "&&"]) if envdelta: cmdline.append("env") cmdline.extend(f"{k}={shquote(v)}" for k, v in envdelta.items()) if isinstance(args, (tuple, list)): cmdline.extend(args) else: cmdline.append(args) return self._ssh_command[tuple(cmdline)].popen(**kwargs) def nohup(self, command): """ Runs the given command using ``nohup`` and redirects std handles, 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, 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): """ Runs the given command using ``nohup`` and redirects std handles, allowing the command to run "detached" from its controlling TTY or parent. Does not return anything. .. versionadded:: 1.6.0 """ if stdout is None: stdout = "/dev/null" if stderr is None: stderr = "&1" args = [] if str(cwd) == "." else ["cd", str(cwd), "&&"] args.append("nohup") args.extend(command.formulate()) args.extend( [ (">>" if append else ">") + str(stdout), "2" + (">>" if (append and stderr != "&1") else ">") + str(stderr), "` object can be used as a *context-manager*. The more conventional use case is the following:: +---------+ +---------+ | Your | | Remote | | Machine | | Machine | +----o----+ +---- ----+ | ^ | | lport dport | | \______SSH TUNNEL____/ (secure) Here, you wish to communicate safely between port ``lport`` of your machine and port ``dport`` of the remote machine. Communication is tunneled over SSH, so the connection is authenticated and encrypted. The more general case is shown below (where ``dport != "localhost"``):: +---------+ +-------------+ +-------------+ | Your | | Remote | | Destination | | Machine | | Machine | | Machine | +----o----+ +---- ----o---+ +---- --------+ | ^ | ^ | | | | lhost:lport | | dhost:dport | | | | \_____SSH TUNNEL_____/ \_____SOCKET____/ (secure) (not secure) Usage:: rem = SshMachine("megazord") with rem.tunnel(1234, "/var/lib/mysql/mysql.sock", dhost=None): sock = socket.socket() sock.connect(("localhost", 1234)) # sock is now tunneled to the MySQL socket on megazord """ formatted_lhost = "" if lhost is None else f"[{lhost}]:" formatted_dhost = "" if dhost is None else f"[{dhost}]:" if str(lport) == "0": lport = _get_free_port() ssh_opts = ( [ "-L", f"{formatted_lhost}{lport}:{formatted_dhost}{dport}", ] if not reverse else [ "-R", f"{formatted_dhost}{dport}:{formatted_lhost}{lport}", ] ) proc = self.popen((), ssh_opts=ssh_opts, new_session=True) return SshTunnel( ShellSession( proc, self.custom_encoding, connect_timeout=self.connect_timeout ), lport, dport, reverse, ) @staticmethod def _translate_drive_letter(path): # replace c:\some\path with /c/some/path path = str(path) if ":" in path: return "/" + path.replace(":", "").replace("\\", "/") return path def download(self, src, dst): if isinstance(src, LocalPath): raise TypeError(f"src of download cannot be {src!r}") if isinstance(src, RemotePath) and src.remote != self: raise TypeError(f"src {src!r} points to a different remote machine") if isinstance(dst, RemotePath): raise TypeError(f"dst of download cannot be {dst!r}") if IS_WIN32: src = self._translate_drive_letter(src) dst = self._translate_drive_letter(dst) self._scp_command(f"{self._fqhost}:{shquote(src)}", dst) def upload(self, src, dst): if isinstance(src, RemotePath): raise TypeError(f"src of upload cannot be {src!r}") if isinstance(dst, LocalPath): raise TypeError(f"dst of upload cannot be {dst!r}") if isinstance(dst, RemotePath) and dst.remote != self: raise TypeError(f"dst {dst!r} points to a different remote machine") if IS_WIN32: src = self._translate_drive_letter(src) dst = self._translate_drive_letter(dst) self._scp_command(src, f"{self._fqhost}:{shquote(dst)}") class PuttyMachine(SshMachine): """ PuTTY-flavored SSH connection. The programs ``plink`` and ``pscp`` are expected to be in the path (or you may provide your own ``ssh_command`` and ``scp_command``) Arguments are the same as for :class:`plumbum.machines.remote.SshMachine` """ def __init__( self, host, user=None, port=None, keyfile=None, ssh_command=None, scp_command=None, ssh_opts=(), scp_opts=(), encoding="utf8", connect_timeout=10, new_session=False, ): if ssh_command is None: ssh_command = local["plink"] if scp_command is None: scp_command = local["pscp"] if not ssh_opts: ssh_opts = ["-ssh"] if user is None: user = local.env.user if port is not None: ssh_opts.extend(["-P", str(port)]) scp_opts = [*list(scp_opts), "-P", str(port)] port = None SshMachine.__init__( self, host, user, port, keyfile=keyfile, ssh_command=ssh_command, scp_command=scp_command, ssh_opts=ssh_opts, scp_opts=scp_opts, encoding=encoding, connect_timeout=connect_timeout, new_session=new_session, ) def __str__(self): return f"putty-ssh://{self._fqhost}" def _translate_drive_letter(self, path): # pscp takes care of windows paths automatically return path def session(self, isatty=False, new_session=False): return ShellSession( self.popen((), (["-t"] if isatty else ["-T"]), new_session=new_session), self.custom_encoding, isatty, self.connect_timeout, ) plumbum-1.8.3/plumbum/path/__init__.py0000644000000000000000000000061314613634536014673 0ustar00from plumbum.path.base import FSUser, Path, RelativePath from plumbum.path.local import LocalPath, LocalWorkdir from plumbum.path.remote import RemotePath, RemoteWorkdir from plumbum.path.utils import copy, delete, move __all__ = ( "FSUser", "Path", "RelativePath", "LocalPath", "LocalWorkdir", "RemotePath", "RemoteWorkdir", "copy", "delete", "move", ) plumbum-1.8.3/plumbum/path/base.py0000644000000000000000000004170214613634536014052 0ustar00import io import itertools import operator import os import typing import warnings from abc import ABC, abstractmethod from functools import reduce FLAGS = {"f": os.F_OK, "w": os.W_OK, "r": os.R_OK, "x": os.X_OK} class FSUser(int): """A special object that represents a file-system user. It derives from ``int``, so it behaves just like a number (``uid``/``gid``), but also have a ``.name`` attribute that holds the string-name of the user, if given (otherwise ``None``) """ def __new__(cls, val, name=None): self = int.__new__(cls, val) self.name = name 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 ` and :class:`RemotePath `. """ CASE_SENSITIVE = True def __repr__(self): return f"<{self.__class__.__name__} {self}>" def __truediv__(self: _PathImpl, other: typing.Any) -> _PathImpl: """Joins two paths""" return self.join(other) def __getitem__(self, key): if type(key) == str or isinstance(key, Path): # noqa: E721 return self / key return str(self)[key] def __floordiv__(self, expr): """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" return self.glob(expr) def __iter__(self): """Iterate over the files in this directory""" return iter(self.list()) def __eq__(self, other: object) -> bool: if isinstance(other, Path): return self._get_info() == other._get_info() if isinstance(other, str): if self.CASE_SENSITIVE: return str(self) == other return str(self).lower() == other.lower() return NotImplemented def __ne__(self, other): return not self == other def __gt__(self, other): return str(self) > str(other) def __ge__(self, other): return str(self) >= str(other) def __lt__(self, other): return str(self) < str(other) def __le__(self, other): return str(self) <= str(other) def __hash__(self): return hash(str(self)) if self.CASE_SENSITIVE else hash(str(self).lower()) def __bool__(self): return bool(str(self)) def __fspath__(self): """Added for Python 3.6 support""" return str(self) def __contains__(self, item): """Paths should support checking to see if an file or folder is in them.""" try: return (self / item.name).exists() except AttributeError: return (self / item).exists() @abstractmethod def _form(self: _PathImpl, *parts: typing.Any) -> _PathImpl: pass def up(self, count=1): """Go up in ``count`` directories (the default is 1)""" return self.join("../" * count) def walk( self, 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 takes a path as an argument and returns a boolean :param filter: the filter (predicate function) for matching results. Only paths matching this predicate are returned. Defaults to everything. :param dir_filter: the filter (predicate function) for matching directories. Only directories matching this predicate are recursed into. Defaults to everything. """ for p in self.list(): if filter(p): yield p if p.is_dir() and dir_filter(p): yield from p.walk(filter, dir_filter) @property @abstractmethod 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, stacklevel=2) return self.name @property @abstractmethod def stem(self) -> str: """The name without an extension, or the last component of the path""" @property @abstractmethod def dirname(self: _PathImpl) -> _PathImpl: """The dirname component of this path""" @property @abstractmethod def root(self) -> str: """The root of the file tree (`/` on Unix)""" @property @abstractmethod def drive(self) -> str: """The drive letter (on Windows)""" @property @abstractmethod def suffix(self) -> str: """The suffix of this file""" @property @abstractmethod def suffixes(self) -> typing.List[str]: """This is a list of all suffixes""" @property @abstractmethod def uid(self) -> FSUser: """The user that owns this path. The returned value is a :class:`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) -> FSUser: """The group that owns this path. The returned value is a :class:`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: typing.Optional[str] = None) -> str: """Returns a universal resource identifier. Use ``scheme`` to force a scheme.""" @abstractmethod def _get_info(self) -> typing.Any: pass @abstractmethod def join(self: _PathImpl, *parts: typing.Any) -> _PathImpl: """Joins this path with any number of paths""" @abstractmethod def list(self: _PathImpl) -> typing.List[_PathImpl]: """Returns the files in this directory""" @abstractmethod 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) -> 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, stacklevel=2) return self.is_dir() @abstractmethod def is_file(self) -> bool: """Returns ``True`` if this path is a regular file, ``False`` otherwise""" def isfile(self) -> bool: """Included for compatibility with older Plumbum code""" 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, stacklevel=2) return self.is_symlink() @abstractmethod def is_symlink(self) -> bool: """Returns ``True`` if this path is a symbolic link, ``False`` otherwise""" @abstractmethod def exists(self) -> bool: """Returns ``True`` if this path exists, ``False`` otherwise""" @abstractmethod def stat(self) -> os.stat_result: """Returns the os.stats for a file""" @abstractmethod def with_name(self: _PathImpl, name: typing.Any) -> _PathImpl: """Returns a path with the name replaced""" @abstractmethod 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 ``depth=None`` is useful""" def preferred_suffix(self, suffix): """Adds a suffix if one does not currently exist (otherwise, no change). Useful for loading files with a default suffix""" return self if len(self.suffixes) > 0 else self.with_suffix(suffix) @abstractmethod 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 def delete(self): """Deletes this path (recursively, if a directory)""" @abstractmethod def move(self, dst): """Moves this path to a different location""" def rename(self, newname): """Renames this path to the ``new name`` (only the basename is changed)""" return self.move(self.up() / newname) @abstractmethod def copy(self, dst, override=None): """Copies this path (recursively, if a directory) to the destination path "dst". Raises TypeError if dst exists and override is False. Will overwrite if override is True. Will silently fail to copy if override is None (the default).""" @abstractmethod def mkdir(self, mode=0o777, parents=True, exist_ok=True): """ Creates a directory at this path. :param mode: **Currently only implemented for local paths!** Numeric mode to use for directory creation, which may be ignored on some systems. The current implementation reproduces the behavior of ``os.mkdir`` (i.e., the current umask is first masked out), but this may change for remote paths. As with ``os.mkdir``, it is recommended to call :func:`chmod` explicitly if you need to be sure. :param parents: If this is true (the default), the directory's parents will also be created if necessary. :param exist_ok: If this is true (the default), no exception will be raised if the directory already exists (otherwise ``OSError``). Note that the defaults for ``parents`` and ``exist_ok`` are the opposite of what they are in Python's own ``pathlib`` - this is to maintain backwards-compatibility with Plumbum's behaviour from before they were implemented. """ @abstractmethod def open( self, mode: str = "r", *, encoding: typing.Optional[str] = None ) -> io.IOBase: """opens this path as a file""" @abstractmethod 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: 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'``""" @abstractmethod def touch(self): """Update the access time. Creates an empty file if none exists.""" @abstractmethod def chown(self, owner=None, group=None, recursive=None): """Change ownership of this path. :param owner: The owner to set (either ``uid`` or ``username``), optional :param group: The group to set (either ``gid`` or ``groupname``), optional :param recursive: whether to change ownership of all contained files and subdirectories. Only meaningful when ``self`` is a directory. If ``None``, the value will default to ``True`` if ``self`` is a directory, ``False`` otherwise. """ @abstractmethod def chmod(self, mode): """Change the mode of path to the numeric mode. :param mode: file mode as for os.chmod """ @staticmethod def _access_mode_to_flags(mode, flags=None): if flags is None: flags = FLAGS if isinstance(mode, str): return reduce(operator.or_, [flags[m] for m in mode.lower()], 0) return mode @abstractmethod 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: ``'f'``, ``'x'``, ``'r'``, ``'w'`` for ``os.F_OK``, ``os.X_OK``, ``os.R_OK``, ``os.W_OK`` """ @abstractmethod def link(self, dst): """Creates a hard link from ``self`` to ``dst`` :param dst: the destination path """ @abstractmethod def symlink(self, dst): """Creates a symbolic link from ``self`` to ``dst`` :param dst: the destination path """ @abstractmethod def unlink(self): """Deletes a symbolic link""" def split(self, *_args, **_kargs): """Splits the path on directory separators, yielding a list of directories, e.g, ``"/var/log/messages"`` will yield ``['var', 'log', 'messages']``. """ parts = [] path = self while path != path.dirname: parts.append(path.name) path = path.dirname return parts[::-1] @property def parts(self): """Splits the directory into parts, including the base directory, returns a tuple""" 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 ``source_path + (target_path - source_path) == target_path``. For example:: /var/log/messages - /var/log/messages = [] /var/log/messages - /var = [log, messages] /var/log/messages - / = [var, log, messages] /var/log/messages - /var/tmp = [.., log, messages] /var/log/messages - /opt = [.., var, log, messages] /var/log/messages - /opt/lib = [.., .., var, log, messages] """ if isinstance(source, str): source = self._form(source) parts = self.split() baseparts = source.split() ancestors = len( list(itertools.takewhile(lambda p: p[0] == p[1], zip(parts, baseparts))) ) return RelativePath([".."] * (len(baseparts) - ancestors) + parts[ancestors:]) def __sub__(self, other): """Same as ``self.relative_to(other)``""" return self.relative_to(other) @staticmethod def _glob(pattern, fn): """Applies a glob string or list/tuple/iterable to the current path, using ``fn``""" if isinstance(pattern, str): return fn(pattern) results = {value for single_pattern in pattern for value in fn(single_pattern)} return sorted(results) 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.""" # TODO: Resolve symlinks here return self @property def parents(self): """Pathlib like sequence of ancestors""" as_list = ( reduce(lambda x, y: self._form(x) / y, self.parts[:i], self.parts[0]) for i in range(len(self.parts) - 1, 0, -1) ) return tuple(as_list) @property def parent(self): """Pathlib like parent of the path.""" return self.parents[0] class RelativePath: """ Relative paths are the "delta" required to get from one path to another. Note that relative path do not point at anything, and thus are not paths. Therefore they are system agnostic (but closed under addition) Paths are always absolute and point at "something", whether existent or not. Relative paths are created by subtracting paths (``Path.relative_to``) """ def __init__(self, parts): self.parts = parts def __str__(self): return "/".join(self.parts) def __iter__(self): return iter(self.parts) def __len__(self): return len(self.parts) def __getitem__(self, index): return self.parts[index] def __repr__(self): return f"RelativePath({self.parts!r})" def __eq__(self, other): return str(self) == str(other) def __ne__(self, other): return not self == other def __gt__(self, other): return str(self) > str(other) def __ge__(self, other): return str(self) >= str(other) def __lt__(self, other): return str(self) < str(other) def __le__(self, other): return str(self) <= str(other) def __hash__(self): return hash(str(self)) def __bool__(self): return bool(str(self)) def up(self, count=1): return RelativePath(self.parts[:-count]) def __radd__(self, path): return path.join(*self.parts) plumbum-1.8.3/plumbum/path/local.py0000644000000000000000000002460214613634536014232 0ustar00import errno import glob import logging import os import shutil import urllib.parse as urlparse import urllib.request as urllib from contextlib import contextmanager from plumbum.lib import IS_WIN32 from plumbum.path.base import FSUser, Path from plumbum.path.remote import RemotePath try: from grp import getgrgid, getgrnam from pwd import getpwnam, getpwuid except ImportError: def getpwuid(_x): # type: ignore[misc] return (None,) def getgrgid(_x): # type: ignore[misc] return (None,) def getpwnam(_x): # type: ignore[misc] raise OSError("`getpwnam` not supported") def getgrnam(_x): # type: ignore[misc] raise OSError("`getgrnam` not supported") logger = logging.getLogger("plumbum.local") _EMPTY = object() # =================================================================================================== # Local Paths # =================================================================================================== class LocalPath(Path): """The class implementing local-machine paths""" CASE_SENSITIVE = not IS_WIN32 def __new__(cls, *parts): if ( len(parts) == 1 and isinstance(parts[0], cls) and not isinstance(parts[0], LocalWorkdir) ): return parts[0] if not parts: 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}") return super().__new__( cls, os.path.normpath(os.path.join(*(str(p) for p in parts))) ) @property def _path(self): return str(self) def _get_info(self): return self._path def _form(self, *parts): return LocalPath(*parts) @property def name(self): return os.path.basename(str(self)) @property def dirname(self): return LocalPath(os.path.dirname(str(self))) @property def suffix(self): return os.path.splitext(str(self))[1] @property def suffixes(self): exts = [] base = str(self) while True: base, ext = os.path.splitext(base) if ext: exts.append(ext) else: return list(reversed(exts)) @property def uid(self): uid = self.stat().st_uid name = getpwuid(uid)[0] return FSUser(uid, name) @property def gid(self): gid = self.stat().st_gid name = getgrgid(gid)[0] return FSUser(gid, name) def join(self, *others): return LocalPath(self, *others) def list(self): return [self / fn for fn in os.listdir(str(self))] def iterdir(self): try: return (self / fn.name for fn in os.scandir(str(self))) except AttributeError: return (self / fn for fn in os.listdir(str(self))) def is_dir(self): return os.path.isdir(str(self)) def is_file(self): return os.path.isfile(str(self)) def is_symlink(self): return os.path.islink(str(self)) def exists(self): return os.path.exists(str(self)) def stat(self): return os.stat(str(self)) def with_name(self, name): return LocalPath(self.dirname) / name @property def stem(self): return self.name.rsplit(os.path.extsep)[0] def with_suffix(self, suffix, depth=1): if suffix and not suffix.startswith(os.path.extsep) or suffix == os.path.extsep: raise ValueError(f"Invalid suffix {suffix!r}") name = self.name depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) for _ in range(depth): name, _ = os.path.splitext(name) return LocalPath(self.dirname) / (name + suffix) def glob(self, pattern): return self._glob( pattern, lambda pat: [ LocalPath(m) for m in glob.glob(os.path.join(glob.escape(str(self)), pat)) ], ) def delete(self): if not self.exists(): return if self.is_dir(): shutil.rmtree(str(self)) else: try: os.remove(str(self)) except OSError as ex: # pragma: no cover # file might already been removed (a race with other threads/processes) if ex.errno != errno.ENOENT: raise def move(self, dst): if isinstance(dst, RemotePath): raise TypeError(f"Cannot move local path {self} to {dst!r}") shutil.move(str(self), str(dst)) return LocalPath(dst) def copy(self, dst, override=None): if isinstance(dst, RemotePath): raise TypeError(f"Cannot copy local path {self} to {dst!r}") dst = LocalPath(dst) if override is False and dst.exists(): raise TypeError("File exists and override was not specified") if override: dst.delete() if self.is_dir(): shutil.copytree(str(self), str(dst)) else: dst_dir = LocalPath(dst).dirname if not dst_dir.exists(): dst_dir.mkdir() shutil.copy2(str(self), str(dst)) return dst def mkdir(self, mode=0o777, parents=True, exist_ok=True): if not self.exists() or not exist_ok: try: if parents: os.makedirs(str(self), mode) else: os.mkdir(str(self), mode) except OSError as ex: # pragma: no cover # directory might already exist (a race with other threads/processes) if ex.errno != errno.EEXIST or not exist_ok: raise def open(self, mode="r", encoding=None): return open( # noqa: SIM115 str(self), mode, encoding=encoding, ) def read(self, encoding=None, mode="r"): if encoding and "b" not in mode: mode = mode + "b" with self.open(mode) as f: data = f.read() if 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: mode = "w" if isinstance(data, str) else "wb" with self.open(mode) as f: f.write(data) def touch(self): with open(str(self), "a", encoding="utf-8"): os.utime(str(self), None) def chown(self, owner=None, group=None, recursive=None): if not hasattr(os, "chown"): raise OSError("os.chown() not supported") uid = ( self.uid if owner is None else (owner if isinstance(owner, int) else getpwnam(owner)[2]) ) gid = ( self.gid if group is None else (group if isinstance(group, int) else getgrnam(group)[2]) ) os.chown(str(self), uid, gid) if recursive or (recursive is None and self.is_dir()): for subpath in self.walk(): os.chown(str(subpath), uid, gid) def chmod(self, mode): if not hasattr(os, "chmod"): raise OSError("os.chmod() not supported") os.chmod(str(self), mode) def access(self, mode=0): return os.access(str(self), self._access_mode_to_flags(mode)) def link(self, dst): if isinstance(dst, RemotePath): raise TypeError( f"Cannot create a hardlink from local path {self} to {dst!r}" ) if hasattr(os, "link"): os.link(str(self), str(dst)) else: from plumbum.machines.local import local # windows: use mklink if self.is_dir(): local["cmd"]("/C", "mklink", "/D", "/H", str(dst), str(self)) else: local["cmd"]("/C", "mklink", "/H", str(dst), str(self)) def symlink(self, dst): if isinstance(dst, RemotePath): raise TypeError( f"Cannot create a symlink from local path {self} to {dst!r}" ) if hasattr(os, "symlink"): os.symlink(str(self), str(dst)) else: from plumbum.machines.local import local # windows: use mklink if self.is_dir(): local["cmd"]("/C", "mklink", "/D", str(dst), str(self)) else: local["cmd"]("/C", "mklink", str(dst), str(self)) def unlink(self): try: if hasattr(os, "symlink") or not self.is_dir(): os.unlink(str(self)) else: # windows: use rmdir for directories and directory symlinks os.rmdir(str(self)) except OSError as ex: # pragma: no cover # file might already been removed (a race with other threads/processes) if ex.errno != errno.ENOENT: raise def as_uri(self, scheme="file"): return urlparse.urljoin(str(scheme) + ":", urllib.pathname2url(str(self))) @property def drive(self): return os.path.splitdrive(str(self))[0] @property def root(self): return os.path.sep class LocalWorkdir(LocalPath): """Working directory manipulator""" def __hash__(self): raise TypeError("unhashable type") def __new__(cls): return super().__new__(cls, os.getcwd()) def chdir(self, newdir): """Changes the current working directory to the given one :param newdir: The destination director (a string or a ``LocalPath``) """ if isinstance(newdir, RemotePath): raise TypeError(f"newdir cannot be {newdir!r}") logger.debug("Chdir to %s", newdir) os.chdir(str(newdir)) return self.__class__() def getpath(self): """Returns the current working directory as a ``LocalPath`` object""" return LocalPath(self._path) @contextmanager def __call__(self, newdir): """A context manager used to ``chdir`` into a directory and then ``chdir`` back to the previous location; much like ``pushd``/``popd``. :param newdir: The destination directory (a string or a ``LocalPath``) """ prev = self._path newdir = self.chdir(newdir) try: yield newdir finally: self.chdir(prev) plumbum-1.8.3/plumbum/path/remote.py0000644000000000000000000002605514613634536014437 0ustar00import errno import os import urllib.request as urllib from contextlib import contextmanager from plumbum.commands import ProcessExecutionError, shquote from plumbum.path.base import FSUser, Path class StatRes: """POSIX-like stat result""" def __init__(self, tup): self._tup = tuple(tup) def __getitem__(self, index): return self._tup[index] st_mode = mode = property(lambda self: self[0]) st_ino = ino = property(lambda self: self[1]) st_dev = dev = property(lambda self: self[2]) st_nlink = nlink = property(lambda self: self[3]) st_uid = uid = property(lambda self: self[4]) st_gid = gid = property(lambda self: self[5]) st_size = size = property(lambda self: self[6]) st_atime = atime = property(lambda self: self[7]) st_mtime = mtime = property(lambda self: self[8]) st_ctime = ctime = property(lambda self: self[9]) class RemotePath(Path): """The class implementing remote-machine paths""" def __new__(cls, remote, *parts): if not parts: raise TypeError("At least one path part is required (none given)") windows = remote.uname.lower() == "windows" normed = [] parts = tuple( map(str, parts) ) # force the paths into string, so subscription works properly # Simple skip if path is absolute if parts[0] and parts[0][0] not in ("/", "\\"): cwd = ( remote._cwd if hasattr(remote, "_cwd") else remote._session.run("pwd")[1].strip() ) parts = (cwd, *parts) for p in parts: if windows: plist = str(p).replace("\\", "/").split("/") else: plist = str(p).split("/") if not plist[0]: plist.pop(0) del normed[:] for item in plist: if item in {"", "."}: continue if item == "..": if normed: normed.pop(-1) else: normed.append(item) if windows: self = super().__new__(cls, "\\".join(normed)) self.CASE_SENSITIVE = False # On this object only else: self = super().__new__(cls, "/" + "/".join(normed)) self.CASE_SENSITIVE = True self.remote = remote return self def _form(self, *parts): return RemotePath(self.remote, *parts) @property def _path(self): return str(self) @property def name(self): if "/" not in str(self): return str(self) return str(self).rsplit("/", 1)[1] @property def dirname(self): if "/" not in str(self): return str(self) return self.__class__(self.remote, str(self).rsplit("/", 1)[0]) @property def suffix(self): return "." + self.name.rsplit(".", 1)[1] @property def suffixes(self): name = self.name exts = [] while "." in name: name, ext = name.rsplit(".", 1) exts.append("." + ext) return list(reversed(exts)) @property def uid(self): uid, name = self.remote._path_getuid(self) return FSUser(int(uid), name) @property def gid(self): gid, name = self.remote._path_getgid(self) return FSUser(int(gid), name) def _get_info(self): return (self.remote, self._path) def join(self, *parts): return RemotePath(self.remote, self, *parts) def list(self): if not self.is_dir(): return [] return [self.join(fn) for fn in self.remote._path_listdir(self)] def iterdir(self): if not self.is_dir(): return () return (self.join(fn) for fn in self.remote._path_listdir(self)) def is_dir(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode == "directory" def is_file(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode in ("regular file", "regular empty file") def is_symlink(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode == "symbolic link" def exists(self): return self.remote._path_stat(self) is not None def stat(self): res = self.remote._path_stat(self) if res is None: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), "") return res def with_name(self, name): return self.__class__(self.remote, self.dirname) / name def with_suffix(self, suffix, depth=1): if suffix and not suffix.startswith(".") or suffix == ".": raise ValueError(f"Invalid suffix {suffix!r}") name = self.name depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) for _ in range(depth): name, _ = name.rsplit(".", 1) return self.__class__(self.remote, self.dirname) / (name + suffix) def glob(self, pattern): return self._glob( pattern, lambda pat: [ RemotePath(self.remote, m) for m in self.remote._path_glob(self, pat) ], ) def delete(self): if not self.exists(): return self.remote._path_delete(self) unlink = delete def move(self, dst): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, str): raise TypeError( f"dst must be a string or a RemotePath (to the same remote machine), got {dst!r}" ) self.remote._path_move(self, dst) def copy(self, dst, override=False): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, str): raise TypeError( f"dst must be a string or a RemotePath (to the same remote machine), got {dst!r}" ) if override: if isinstance(dst, str): dst = RemotePath(self.remote, dst) dst.delete() else: if isinstance(dst, str): dst = RemotePath(self.remote, dst) if dst.exists(): raise TypeError("Override not specified and dst exists") self.remote._path_copy(self, dst) def mkdir(self, mode=None, parents=True, exist_ok=True): if parents and exist_ok: self.remote._path_mkdir(self, mode=mode, minus_p=True) else: if parents and len(self.parts) > 1: self.remote._path_mkdir(self.parent, mode=mode, minus_p=True) try: self.remote._path_mkdir(self, mode=mode, minus_p=False) except ProcessExecutionError as ex: if "File exists" not in ex.stderr: raise if not exist_ok: raise OSError( errno.EEXIST, "File exists (on remote end)", str(self) ) from None def read(self, encoding=None): data = self.remote._path_read(self) if encoding: return data.decode(encoding) return data def write(self, data, encoding=None): if encoding: data = data.encode(encoding) self.remote._path_write(self, data) def touch(self): self.remote._path_touch(str(self)) def chown(self, owner=None, group=None, recursive=None): self.remote._path_chown( self, owner, group, self.is_dir() if recursive is None else recursive ) def chmod(self, mode): self.remote._path_chmod(mode, self) def access(self, mode=0): mode = self._access_mode_to_flags(mode) res = self.remote._path_stat(self) if res is None: return False mask = res.st_mode & 0x1FF return ((mask >> 6) & mode) or ((mask >> 3) & mode) def link(self, dst): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, str): raise TypeError( f"dst must be a string or a RemotePath (to the same remote machine), got {dst!r}" ) self.remote._path_link(self, dst, False) def symlink(self, dst): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, str): raise TypeError( "dst must be a string or a RemotePath (to the same remote machine), got {dst!r}" ) self.remote._path_link(self, dst, True) def open(self, mode="r", bufsize=-1, *, encoding=None): """ Opens this path as a file. Only works for ParamikoMachine-associated paths for now. """ if encoding is not None: raise NotImplementedError( "encoding not supported for ParamikoMachine paths" ) if hasattr(self.remote, "sftp") and hasattr(self.remote.sftp, "open"): return self.remote.sftp.open(self, mode, bufsize) raise NotImplementedError( "RemotePath.open only works for ParamikoMachine-associated paths for now" ) def as_uri(self, scheme="ssh"): suffix = urllib.pathname2url(str(self)) return f"{scheme}://{self.remote._fqhost}{suffix}" @property def stem(self): return self.name.rsplit(".")[0] @property def root(self): return "/" @property def drive(self): return "" class RemoteWorkdir(RemotePath): """Remote working directory manipulator""" def __new__(cls, remote): return super().__new__(cls, remote, remote._session.run("pwd")[1].strip()) def __hash__(self): raise TypeError("unhashable type") def chdir(self, newdir): """Changes the current working directory to the given one""" self.remote._session.run(f"cd {shquote(newdir)}") if hasattr(self.remote, "_cwd"): del self.remote._cwd return self.__class__(self.remote) def getpath(self): """Returns the current working directory as a `remote path ` object""" return RemotePath(self.remote, self) @contextmanager def __call__(self, newdir): """A context manager used to ``chdir`` into a directory and then ``chdir`` back to the previous location; much like ``pushd``/``popd``. :param newdir: The destination director (a string or a :class:`RemotePath `) """ prev = self._path changed_dir = self.chdir(newdir) try: yield changed_dir finally: self.chdir(prev) plumbum-1.8.3/plumbum/path/utils.py0000644000000000000000000000671414613634536014304 0ustar00import os from plumbum.machines.local import local from plumbum.path.base import Path from plumbum.path.local import LocalPath def delete(*paths): """Deletes the given paths. The arguments can be either strings, :class:`local paths `, :class:`remote paths `, or iterables of such. No error is raised if any of the paths does not exist (it is silently ignored) """ for p in paths: if isinstance(p, Path): p.delete() elif isinstance(p, str): local.path(p).delete() elif hasattr(p, "__iter__"): delete(*p) else: raise TypeError(f"Cannot delete {p!r}") def _move(src, dst): ret = copy(src, dst) delete(src) return ret def move(src, dst): """Moves the source path onto the destination path; ``src`` and ``dst`` can be either strings, :class:`LocalPaths ` or :class:`RemotePath `; any combination of the three will work. .. versionadded:: 1.3 ``src`` can also be a list of strings/paths, in which case ``dst`` must not exist or be a directory. """ if not isinstance(dst, Path): dst = local.path(dst) if isinstance(src, (tuple, list)): if not dst.exists(): dst.mkdir() elif not dst.is_dir(): raise ValueError( f"When using multiple sources, dst {dst!r} must be a directory" ) for src2 in src: move(src2, dst) return dst if not isinstance(src, Path): src = local.path(src) if isinstance(src, LocalPath): return src.move(dst) if isinstance(dst, LocalPath) else _move(src, dst) if isinstance(dst, LocalPath): return _move(src, dst) if src.remote == dst.remote: return src.move(dst) return _move(src, dst) def copy(src, dst): """ Copy (recursively) the source path onto the destination path; ``src`` and ``dst`` can be either strings, :class:`LocalPaths ` or :class:`RemotePath `; any combination of the three will work. .. versionadded:: 1.3 ``src`` can also be a list of strings/paths, in which case ``dst`` must not exist or be a directory. """ if not isinstance(dst, Path): dst = local.path(dst) if isinstance(src, (tuple, list)): if not dst.exists(): dst.mkdir() elif not dst.is_dir(): raise ValueError( f"When using multiple sources, dst {dst!r} must be a directory" ) for src2 in src: copy(src2, dst) return dst if not isinstance(src, Path): src = local.path(src) if isinstance(src, LocalPath): if isinstance(dst, LocalPath): return src.copy(dst) dst.remote.upload(src, dst) return dst if isinstance(dst, LocalPath): src.remote.download(src, dst) return dst if src.remote == dst.remote: return src.copy(dst) with local.tempdir() as tmp: copy(src, tmp) copy(tmp / src.name, dst) return dst def gui_open(filename): """This selects the proper gui open function. This can also be achieved with webbrowser, but that is not supported.""" if hasattr(os, "startfile"): os.startfile(filename) else: local.get("xdg-open", "open")(filename) plumbum-1.8.3/tests/_test_paramiko.py0000644000000000000000000000046314613634536014665 0ustar00from plumbum import local from plumbum.paramiko_machine import ParamikoMachine as PM local.env.path.append("c:\\progra~1\\git\\bin") from plumbum.cmd import grep, ls # noqa: E402 m = PM("192.168.1.143") mls = m["ls"] mgrep = m["grep"] # (mls | mgrep["b"])() (mls | grep["\\."])() (ls | mgrep["\\."])() plumbum-1.8.3/tests/conftest.py0000644000000000000000000001006114613634536013504 0ustar00import itertools import logging import os import re import tempfile import pytest SDIR = os.path.dirname(os.path.abspath(__file__)) @pytest.fixture() def testdir(): os.chdir(SDIR) @pytest.fixture() def cleandir(): newpath = tempfile.mkdtemp() os.chdir(newpath) # Pulled from https://github.com/reece/pytest-optional-tests """implements declaration of optional tests using pytest markers The MIT License (MIT) Copyright (c) 2019 Reece Hart 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. """ _logger = logging.getLogger(__name__) marker_re = re.compile(r"^\s*(?P\w+)(:\s*(?P.*))?") def pytest_addoption(parser): group = parser.getgroup("collect") group.addoption( "--run-optional-tests", action="append", dest="run_optional_tests", default=None, help="Optional test markers to run, multiple and/or comma separated okay", ) parser.addini( "optional_tests", "list of optional markers", type="linelist", default="" ) def pytest_configure(config): # register all optional tests declared in ini file as markers # https://docs.pytest.org/en/latest/writing_plugins.html#registering-custom-markers ot_ini = config.inicfg.get("optional_tests").strip().splitlines() for ot_ in ot_ini: # ot should be a line like "optmarker: this is an opt marker", as with markers section config.addinivalue_line("markers", ot_) ot_markers = {marker_re.match(ln).group(1) for ln in ot_ini} # collect requested optional tests ot_run = config.getoption("run_optional_tests") if ot_run: ot_run = list(itertools.chain.from_iterable(a.split(",") for a in ot_run)) else: ot_run = config.inicfg.get("run_optional_tests", []) if ot_run: ot_run = list(re.split(r"[,\s]+", ot_run)) ot_run = set(ot_run) _logger.info("optional tests to run: %s", ot_run) if ot_run: unknown_tests = ot_run - ot_markers if unknown_tests: raise ValueError( "Requested execution of undeclared optional tests: {}".format( ", ".join(unknown_tests) ) ) config._ot_markers = set(ot_markers) config._ot_run = set(ot_run) def pytest_collection_modifyitems(config, items): # https://stackoverflow.com/a/50114028/342839 ot_markers = config._ot_markers ot_run = config._ot_run skips = {} for item in items: marker_names = {m.name for m in item.iter_markers()} if not marker_names: continue test_otms = marker_names & ot_markers if not test_otms: # test is not marked with any optional marker continue if test_otms & ot_run: # test is marked with an enabled optional test; don't skip continue mns = str(marker_names) if mns not in skips: skips[mns] = pytest.mark.skip( reason="Skipping; marked with disabled optional tests ({})".format( ", ".join(marker_names) ) ) item.add_marker(skips[mns]) plumbum-1.8.3/tests/env.py0000644000000000000000000000056014613634536012452 0ustar00import os import platform import sys LINUX = sys.platform.startswith("linux") MACOS = sys.platform.startswith("darwin") WIN = sys.platform.startswith("win32") or sys.platform.startswith("cygwin") CPYTHON = platform.python_implementation() == "CPython" PYPY = platform.python_implementation() == "PyPy" IS_A_TTY = sys.stdin.isatty() HAS_CHOWN = hasattr(os, "chown") plumbum-1.8.3/tests/file with space.txt0000644000000000000000000000000014613634536014765 0ustar00plumbum-1.8.3/tests/slow_process.bash0000755000000000000000000000022114613634536014666 0ustar00#!/usr/bin/env bash echo "Starting test" > slow_process.out for i in $(seq 1 3) do echo $i echo $i >> slow_process.out sleep 1 done plumbum-1.8.3/tests/test_3_cli.py0000644000000000000000000000134014613634536013707 0ustar00from plumbum import cli class Main3Validator(cli.Application): def main(self, myint: int, myint2: int, *mylist: int): print(myint, myint2, mylist) class TestProg3: def test_prog(self, capsys): _, rc = Main3Validator.run(["prog", "1", "2", "3", "4", "5"], exit=False) assert rc == 0 assert "1 2 (3, 4, 5)" in capsys.readouterr()[0] class Main4Validator(cli.Application): def main(self, myint: int, myint2: int, *mylist: "int") -> None: print(myint, myint2, mylist) class TestProg4: def test_prog(self, capsys): _, rc = Main4Validator.run(["prog", "1", "2", "3", "4", "5"], exit=False) assert rc == 0 assert "1 2 (3, 4, 5)" in capsys.readouterr()[0] plumbum-1.8.3/tests/test_cli.py0000644000000000000000000002645414613634536013502 0ustar00from plumbum import cli, local from plumbum.cli.terminal import get_terminal_size class SimpleApp(cli.Application): @cli.switch(["a"]) def spam(self): print("!!a") @cli.switch( ["b", "bacon"], argtype=int, mandatory=True, envname="PLUMBUM_TEST_BACON" ) def bacon(self, param): """give me some bacon""" print("!!b", param) eggs = cli.SwitchAttr( ["e"], str, help="sets the eggs attribute", envname="PLUMBUM_TEST_EGGS" ) cheese = cli.Flag(["--cheese"], help="cheese, please") chives = cli.Flag(["--chives"], help="chives, instead") verbose = cli.CountOf(["v"], help="increases the verbosity level") benedict = cli.CountOf( ["--benedict"], help="""a very long help message with lots of useless information that nobody would ever want to read, but heck, we need to test text wrapping in help messages as well""", ) csv = cli.SwitchAttr( ["--csv"], cli.Set("MIN", "MAX", int, csv=True, all_markers={"all"}) ) num = cli.SwitchAttr(["--num"], cli.Set("MIN", "MAX", int)) def main(self, *args): old = self.eggs self.eggs = "lalala" self.eggs = old self.tailargs = args print(self.csv) class PositionalApp(cli.Application): def main(self, one): print("Got", one) class Geet(cli.Application): debug = cli.Flag("--debug") cleanups = [] def main(self): del self.cleanups[:] print("hi this is geet main") def cleanup(self, retcode): self.cleanups.append(1) print(f"geet cleaning up with rc = {retcode}") @Geet.subcommand("add") class GeetAdd(cli.Application): def main(self, *files): return "adding", files @Geet.subcommand("commit") class GeetCommit(cli.Application): message = cli.Flag("-m", str) def main(self): if self.parent.debug: return "committing in debug" return "committing" def cleanup(self, retcode): self.parent.cleanups.append(2) print(f"geet commit cleaning up with rc = {retcode}") class Sample(cli.Application): DESCRIPTION = "A sample cli application" DESCRIPTION_MORE = """ ABC This is just a sample help text typed with a Dvorak keyboard. Although this paragraph is not left or right justified in source, we expect it to appear formatted nicely on the output, maintaining the indentation of the first line. DEF this one has a different indentation. Let's test that list items are not combined as paragraphs. - Item 1 GHI more text for item 1, which may be very very very very very very long and even more long and long and long to prove that we can actually wrap list items as well. - Item 2 and this is some text for item 2 - Item 3 List items with invisible bullets should be printed without the bullet. /XYZ Invisible 1 /Invisible 2 * Star 1 * Star 2 Last paragraph can fill more than one line on the output as well. So many features is bound to cause lots of bugs. Oh well... """ foo = cli.SwitchAttr("--foo") Sample.unbind_switches("--version") class Mumble(cli.Application): pass Sample.subcommand("mumble", Mumble) class LazyLoaded(cli.Application): def main(self): print("hello world") class AppA(cli.Application): @cli.switch(["--one"]) def one(self): pass two = cli.SwitchAttr(["--two"]) class AppB(AppA): @cli.switch(["--three"]) def three(self): pass four = cli.SwitchAttr(["--four"]) def main(self): pass # Testing #363 class TestInheritedApp: def test_help(self, capsys): _, rc = AppB.run(["AppB", "-h"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "--one" in stdout assert "--two" in stdout assert "--three" in stdout assert "--four" in stdout class TestCLI: def test_meta_switches(self): _, rc = SimpleApp.run(["foo", "-h"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--version"], exit=False) assert rc == 0 def test_okay(self, capsys): _, rc = SimpleApp.run(["foo", "--bacon=81"], exit=False) assert rc == 0 inst, rc = SimpleApp.run( [ "foo", "--bacon=81", "-a", "-v", "-e", "7", "-vv", "--", "lala", "-e", "7", ], exit=False, ) assert rc == 0 assert inst.eggs == "7" _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=100"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=MAX,MIN,100"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=100"], exit=False) assert rc == 0 capsys.readouterr() _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=all,100"], exit=False) assert rc == 0 output = capsys.readouterr() assert "min" in output.out assert "max" in output.out assert "100" in output.out _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MAX"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MIN"], exit=False) assert rc == 0 def test_failures(self): _, rc = SimpleApp.run(["foo"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=xx"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=xx"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MOO"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MIN,MAX"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=10.5"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=hello"], exit=False) assert rc == 2 # Testing #371 def test_extra_args(self, capsys): _, rc = PositionalApp.run(["positionalapp"], exit=False) assert rc != 0 stdout, stderr = capsys.readouterr() assert "Expected at least" in stdout _, rc = PositionalApp.run(["positionalapp", "one"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() _, rc = PositionalApp.run(["positionalapp", "one", "two"], exit=False) assert rc != 0 stdout, stderr = capsys.readouterr() assert "Expected at most" in stdout def test_subcommands(self): _, rc = Geet.run(["geet", "--debug"], exit=False) assert rc == 0 assert Geet.cleanups == [1] _, rc = Geet.run(["geet", "--debug", "add", "foo.txt", "bar.txt"], exit=False) assert rc == ("adding", ("foo.txt", "bar.txt")) assert Geet.cleanups == [1] _, rc = Geet.run(["geet", "--debug", "commit"], exit=False) assert rc == "committing in debug" assert Geet.cleanups == [2, 1] _, rc = Geet.run(["geet", "--help"], exit=False) assert rc == 0 _, rc = Geet.run(["geet", "commit", "--help"], exit=False) assert rc == 0 assert Geet.cleanups == [1] def test_help_all(self, capsys): _, rc = Geet.run(["geet", "--help-all"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "--help-all" in stdout assert "geet add" in stdout assert "geet commit" in stdout def test_unbind(self, capsys): _, rc = Sample.run(["sample", "--help"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "--foo" in stdout assert "--version" not in stdout def test_description(self, capsys): _, rc = Sample.run(["sample", "--help"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() cols, _ = get_terminal_size() if cols < 9: # Terminal is too narrow to test pass else: # Paragraph indentation should be preserved assert " ABC" in stdout assert " DEF" in stdout assert " - Item" in stdout # List items should not be combined into paragraphs 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 # List item with invisible bullet should be indented without the bullet assert " XYZ" in stdout def test_default_main(self, capsys): _, rc = Sample.run(["sample"], exit=False) assert rc == 1 stdout, stderr = capsys.readouterr() assert "No sub-command given" in stdout _, rc = Sample.run(["sample", "pimple"], exit=False) assert rc == 1 stdout, stderr = capsys.readouterr() assert "Unknown sub-command 'pimple'" in stdout _, rc = Sample.run(["sample", "mumble"], exit=False) assert rc == 1 stdout, stderr = capsys.readouterr() assert "main() not implemented" in stdout def test_lazy_subcommand(self, capsys): class Foo(cli.Application): pass Foo.subcommand("lazy", "test_cli.LazyLoaded") _, rc = Foo.run(["foo", "lazy"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "hello world" in stdout def test_reset_switchattr(self): inst, rc = SimpleApp.run(["foo", "--bacon=81", "-e", "bar"], exit=False) assert rc == 0 assert inst.eggs == "bar" inst, rc = SimpleApp.run(["foo", "--bacon=81"], exit=False) assert rc == 0 assert inst.eggs is None def test_invoke(self): inst, rc = SimpleApp.invoke("arg1", "arg2", eggs="sunny", bacon=10, verbose=2) assert (inst.eggs, inst.verbose, inst.tailargs) == ( "sunny", 2, ("arg1", "arg2"), ) def test_env_var(self, capsys): _, rc = SimpleApp.run(["arg", "--bacon=10"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "10" in stdout with local.env( PLUMBUM_TEST_BACON="20", PLUMBUM_TEST_EGGS="raw", ): inst, rc = SimpleApp.run(["arg"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "20" in stdout assert inst.eggs == "raw" def test_mandatory_env_var(self, capsys): _, rc = SimpleApp.run(["arg"], exit=False) assert rc == 2 stdout, stderr = capsys.readouterr() assert "bacon is mandatory" in stdout def test_partial_switches(self, capsys): app = SimpleApp app.ALLOW_ABBREV = True inst, rc = app.run(["foo", "--bacon=2", "--ch"], exit=False) stdout, stderr = capsys.readouterr() assert "Ambiguous partial switch" in stdout assert rc == 2 inst, rc = app.run(["foo", "--bacon=2", "--chee"], exit=False) assert rc == 0 assert inst.cheese is True assert inst.chives is False plumbum-1.8.3/tests/test_clicolor.py0000644000000000000000000000534414613634536014534 0ustar00from plumbum import cli, colors colors.use_color = 3 def make_app(): class SimpleApp(cli.Application): PROGNAME = colors.green VERSION = colors.red | "1.0.3" @cli.switch(["a"]) def spam(self): print("!!a") def main(self, *args): print("lalala") return SimpleApp class TestSimpleApp: def test_runs(self): SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp"], exit=False) assert rc == 0 def test_colorless_run(self, capsys): colors.use_color = 0 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp"], exit=False) assert capsys.readouterr()[0] == "lalala\n" def test_colorful_run(self, capsys): colors.use_color = 4 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp"], exit=False) assert capsys.readouterr()[0] == "lalala\n" def test_colorless_output(self, capsys): colors.use_color = 0 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp", "-h"], exit=False) output = capsys.readouterr()[0] assert "SimpleApp 1.0.3" in output assert "SimpleApp [SWITCHES] args..." in output def test_colorful_help(self, capsys): colors.use_color = 4 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp", "-h"], exit=False) output = capsys.readouterr()[0] assert "SimpleApp 1.0.3" not in output assert SimpleApp.PROGNAME | "SimpleApp" in output class TestNSApp: def test_colorful_output(self, capsys): colors.use_color = 4 class NotSoSimpleApp(cli.Application): PROGNAME = colors.blue | "NSApp" VERSION = "1.2.3" COLOR_GROUPS = {"Switches": colors.cyan} COLOR_GROUP_TITLES = {"Switches": colors.bold & colors.cyan} COLOR_USAGE_TITLE = colors.bold & colors.cyan @cli.switch(["b"], help="this is a bacon switch") def bacon(self): print("Oooooh, I love BACON!") @cli.switch(["c"], help=colors.red | "crunchy") def crunchy(self): print("Crunchy...") def main(self): print("Eating!") _, rc = NotSoSimpleApp.run(["NotSoSimpleApp", "-h"], exit=False) output = capsys.readouterr()[0] assert rc == 0 expected = str((colors.blue | "NSApp") + " 1.2.3") assert str(colors.bold & colors.cyan | "Switches:") in output assert str(colors.bold & colors.cyan | "Usage:") in output assert "-b" in output assert str(colors.red | "crunchy") in output assert str(colors.cyan | "this is a bacon switch") in output assert expected in output plumbum-1.8.3/tests/test_color.py0000644000000000000000000000537714613634536014052 0ustar00# Just check to see if this file is importable from plumbum.cli.image import Image # noqa: F401 from plumbum.colorlib.names import FindNearest, color_html from plumbum.colorlib.styles import ( # noqa: F401 ANSIStyle, AttributeNotFound, Color, ColorNotFound, ) class TestNearestColor: def test_exact(self): assert FindNearest(0, 0, 0).all_fast() == 0 for n, color in enumerate(color_html): # Ignoring duplicates if n not in (16, 21, 46, 51, 196, 201, 226, 231, 244): rgb = (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) assert FindNearest(*rgb).all_fast() == n def test_nearby(self): assert FindNearest(1, 2, 2).all_fast() == 0 assert FindNearest(7, 7, 9).all_fast() == 232 def test_simplecolor(self): assert FindNearest(1, 2, 4).only_basic() == 0 assert FindNearest(0, 255, 0).only_basic() == 2 assert FindNearest(100, 100, 0).only_basic() == 3 assert FindNearest(140, 140, 140).only_basic() == 7 class TestColorLoad: def test_rgb(self): blue = Color(0, 0, 255) # Red, Green, Blue assert blue.rgb == (0, 0, 255) def test_simple_name(self): green = Color.from_simple("green") assert green.number == 2 def test_different_names(self): assert Color("Dark Blue") == Color("Dark_Blue") assert Color("Dark_blue") == Color("Dark_Blue") assert Color("DARKBLUE") == Color("Dark_Blue") assert Color("DarkBlue") == Color("Dark_Blue") assert Color("Dark Green") == Color("Dark_Green") def test_loading_methods(self): assert Color("Yellow") == Color.from_full("Yellow") assert ( Color.from_full("yellow").representation != Color.from_simple("yellow").representation ) class TestANSIColor: @classmethod def setup_class(cls): ANSIStyle.use_color = True def test_ansi(self): assert str(ANSIStyle(fgcolor=Color("reset"))) == "\033[39m" assert str(ANSIStyle(fgcolor=Color.from_full("green"))) == "\033[38;5;2m" assert str(ANSIStyle(fgcolor=Color.from_simple("red"))) == "\033[31m" class TestNearestColorAgain: def test_allcolors(self): myrange = ( 0, 1, 2, 5, 17, 39, 48, 73, 82, 140, 193, 210, 240, 244, 250, 254, 255, ) for r in myrange: for g in myrange: for b in myrange: near = FindNearest(r, g, b) assert near.all_slow() == near.all_fast(), f"Tested: {r}, {g}, {b}" plumbum-1.8.3/tests/test_config.py0000644000000000000000000000373514613634536014175 0ustar00import pytest from plumbum import local from plumbum.cli import Config, ConfigINI fname = "test_config.ini" @pytest.mark.usefixtures("cleandir") class TestConfig: def test_makefile(self): with ConfigINI(fname) as conf: conf["value"] = 12 conf["string"] = "ho" with open(fname) as f: contents = f.read() assert "value = 12" in contents assert "string = ho" in contents def test_readfile(self): with open(fname, "w") as f: print( """ [DEFAULT] one = 1 two = hello""", file=f, ) with ConfigINI(fname) as conf: assert conf["one"] == "1" assert conf["two"] == "hello" def test_complex_ini(self): with Config(fname) as conf: conf["value"] = "normal" conf["newer.value"] = "other" with Config(fname) as conf: assert conf["value"] == "normal" assert conf["DEFAULT.value"] == "normal" assert conf["newer.value"] == "other" def test_nowith(self): conf = ConfigINI(fname) conf["something"] = "nothing" conf.write() with open(fname) as f: contents = f.read() assert "something = nothing" in contents def test_home(self): mypath = local.env.home / "some_simple_home_rc.ini" assert not mypath.exists() try: with Config("~/some_simple_home_rc.ini") as conf: conf["a"] = "b" assert mypath.exists() mypath.unlink() with Config(mypath) as conf: conf["a"] = "b" assert mypath.exists() mypath.unlink() finally: mypath.unlink() def test_notouch(self): ConfigINI(fname) assert not local.path(fname).exists() def test_only_string(self): conf = ConfigINI(fname) value = conf.get("value", 2) assert value == "2" plumbum-1.8.3/tests/test_env.py0000644000000000000000000000406714613634536013517 0ustar00import contextlib from plumbum import local from plumbum._testtools import skip_on_windows with contextlib.suppress(ModuleNotFoundError): from plumbum.cmd import printenv @skip_on_windows class TestEnv: def test_change_env(self): with local.env(silly=12): 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) assert localenv == actual assert len(local.env) == len(actual) def test_dictlike(self): keys = {x.split("=")[0] for x in printenv().splitlines() if "=" in x} values = { x.split("=", 1)[1].strip() for x in printenv().splitlines() if "=" in x } assert keys == set(local.env.keys()) assert len(values) == len(set(local.env.values())) def test_custom_env(self): with local.env(): items = {"one": "OnE", "tww": "TWOO"} local.env.update(items) assert "tww" in local.env local.env.clear() assert "tww" not in local.env def test_item(self): with local.env(): local.env["simple_plum"] = "thing" assert "simple_plum" in local.env del local.env["simple_plum"] assert "simple_plum" not in local.env local.env["simple_plum"] = "thing" assert "simple_plum" in local.env 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 @skip_on_windows def test_home(self): assert local.env.home == local.env["HOME"] old_home = local.env.home with local.env(): local.env.home = "Nobody" assert local.env.home == local.env["HOME"] assert local.env.home == "Nobody" assert local.env.home == old_home @skip_on_windows def test_user(self): assert local.env.user plumbum-1.8.3/tests/test_factories.py0000644000000000000000000001432214613634536014701 0ustar00#!/usr/bin/env python3 import pytest from plumbum import colors from plumbum.colorlib import htmlcolors from plumbum.colorlib.styles import ANSIStyle as Style from plumbum.colorlib.styles import ColorNotFound class TestImportColors: def testDifferentImports(self): from plumbum.colors import bold from plumbum.colors.fg import red assert str(red) == str(colors.red) assert str(bold) == str(colors.bold) class TestANSIColor: def setup_method(self, method): # noqa: ARG002 colors.use_color = True def testColorSlice(self): vals = colors[:8] assert len(vals) == 8 assert vals[1] == colors.red vals = colors[40:50] assert len(vals) == 10 assert vals[1] == colors.full(41) def testLoadNumericalColor(self): assert colors.full(2) == colors[2] assert colors.simple(2) == colors(2) assert colors(54) == colors[54] assert colors(1, 30, 77) == colors.rgb(1, 30, 77) assert colors[1, 30, 77] == colors.rgb(1, 30, 77) def testColorStrings(self): assert colors.reset == "\033[0m" assert colors.bold == "\033[1m" assert colors.fg.reset == "\033[39m" def testNegateIsReset(self): assert colors.reset == ~colors assert colors.fg.reset == ~colors.fg assert colors.bg.reset == ~colors.bg def testFromPreviousColor(self): assert colors(colors.red) == colors.red assert colors(colors.bg.red) == colors.bg.red assert colors(colors.bold) == colors.bold def testFromCode(self): assert colors("\033[31m") == colors.red def testEmptyStyle(self): assert str(colors()) == "" assert str(colors("")) == "" assert str(colors(None)) == "" def testLoadColorByName(self): assert colors["LightBlue"] == colors.fg["LightBlue"] assert colors.bg["light_green"] == colors.bg["LightGreen"] assert colors["DeepSkyBlue1"] == colors["#00afff"] assert colors["DeepSkyBlue1"] == colors.hex("#00afff") assert colors["DeepSkyBlue1"] == colors[39] assert colors.DeepSkyBlue1 == colors[39] assert colors.deepskyblue1 == colors[39] assert colors.Deep_Sky_Blue1 == colors[39] assert colors.red == colors.RED with pytest.raises(AttributeError): colors.Notacolorsatall # noqa: B018 def testMultiColor(self): sumcolors = colors.bold & colors.blue assert colors.bold.reset & colors.fg.reset == ~sumcolors def testSums(self): # Sums should not be communitave, last one is used assert colors.red == colors.blue & colors.red assert colors.bg.green == colors.bg.red & colors.bg.green def testRepresentations(self): colors1 = colors.full(87) assert colors1 == colors.DarkSlateGray2 assert colors1.basic == colors.DarkSlateGray2 assert str(colors1.basic) == str(colors.LightGray) colors2 = colors.rgb(1, 45, 214) assert str(colors2.full) == str(colors.Blue3A) def testFromAnsi(self): for c in colors[1:7]: assert c == colors.from_ansi(str(c)) for c in colors.bg[1:7]: assert c == colors.from_ansi(str(c)) for c in colors: assert c == colors.from_ansi(str(c)) for c in colors.bg: assert c == colors.from_ansi(str(c)) for c in colors[:16]: assert c == colors.from_ansi(str(c)) for c in colors.bg[:16]: assert c == colors.from_ansi(str(c)) for c in (colors.bold, colors.underline, colors.italics): assert c == colors.from_ansi(str(c)) col = colors.bold & colors.fg.green & colors.bg.blue & colors.underline assert col == colors.from_ansi(str(col)) col = colors.reset assert col == colors.from_ansi(str(col)) def testWrappedColor(self): string = "This is a string" wrapped = "\033[31mThis is a string\033[39m" assert colors.red.wrap(string) == wrapped assert colors.red | string == wrapped assert colors.red[string] == wrapped newcolors = colors.blue & colors.underline assert newcolors[string] == string | newcolors assert newcolors.wrap(string) == string | colors.blue & colors.underline def testUndoColor(self): 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 ~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 ~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"] == "" def testFromHex(self): with pytest.raises(ColorNotFound): colors.hex("asdf") with pytest.raises(ColorNotFound): colors.hex("#1234Z2") with pytest.raises(ColorNotFound): colors.hex(12) def testDirectCall(self, capsys): colors.blue() assert capsys.readouterr()[0] == str(colors.blue) def testPrint(self, capsys): colors.yellow.print("This is printed to stdout", end="") assert capsys.readouterr()[0] == str( colors.yellow.wrap("This is printed to stdout") ) class TestHTMLColor: def test_html(self): red_tagged = 'This is tagged' assert htmlcolors.red["This is tagged"] == red_tagged assert "This is tagged" | htmlcolors.red == red_tagged twin_tagged = 'This is tagged' assert "This is tagged" | htmlcolors.red & htmlcolors.em == twin_tagged assert "This is tagged" | htmlcolors.em & htmlcolors.red == twin_tagged assert htmlcolors.em & htmlcolors.red | "This is tagged" == twin_tagged plumbum-1.8.3/tests/test_local.py0000644000000000000000000010573314613634536014023 0ustar00import os import pickle import signal import sys import time from pathlib import Path import pytest import plumbum from plumbum import ( BG, ERROUT, FG, RETCODE, TEE, TF, CommandNotFound, LocalPath, ProcessExecutionError, ProcessLineTimedOut, ProcessTimedOut, local, ) from plumbum._testtools import skip_on_windows, skip_without_chown, skip_without_tty from plumbum.fs.atomic import AtomicCounterFile, AtomicFile, PidFile from plumbum.lib import IS_WIN32 from plumbum.machines.local import LocalCommand, PlumbumLocalPopen from plumbum.path import RelativePath # This is a string since we are testing local paths SDIR = os.path.dirname(os.path.abspath(__file__)) class TestLocalPopen: def test_contextmanager(self): command = ["dir"] if IS_WIN32 else ["ls"] with PlumbumLocalPopen(command): pass class TestLocalPath: longpath = local.path("/some/long/path/to/file.txt") def test_name(self): name = self.longpath.name assert isinstance(name, str) assert str(name) == "file.txt" def test_dirname(self): name = self.longpath.dirname assert isinstance(name, LocalPath) assert ( str(name).replace("\\", "/").lstrip("C:").lstrip("D:") == "/some/long/path/to" ) def test_uri(self): if IS_WIN32: pth = self.longpath.as_uri() assert pth.startswith("file:///") assert pth.endswith(":/some/long/path/to/file.txt") else: assert self.longpath.as_uri() == "file:///some/long/path/to/file.txt" def test_pickle(self): path1 = local.path(".") path2 = local.path("~") assert pickle.loads(pickle.dumps(self.longpath)) == self.longpath assert pickle.loads(pickle.dumps(path1)) == path1 assert pickle.loads(pickle.dumps(path2)) == path2 def test_empty(self): with pytest.raises(TypeError): LocalPath() assert local.path() == local.path(".") @skip_without_chown def test_chown(self): with local.tempdir() as dir: p = dir / "foo.txt" p.write(b"hello") assert p.uid == os.getuid() assert p.gid == os.getgid() p.chown(p.uid.name) assert p.uid == os.getuid() def test_split(self): p = local.path("/var/log/messages") assert p.split() == ["var", "log", "messages"] def test_suffix(self): # This picks up the drive letter differently if not constructed here p1 = local.path("/some/long/path/to/file.txt") p2 = local.path("file.tar.gz") assert p1.suffix == ".txt" assert p1.suffixes == [".txt"] assert p2.suffix == ".gz" assert p2.suffixes == [".tar", ".gz"] assert p1.with_suffix(".tar.gz") == local.path("/some/long/path/to/file.tar.gz") assert p2.with_suffix(".other") == local.path("file.tar.other") assert p2.with_suffix(".other", 2) == local.path("file.other") assert p2.with_suffix(".other", 0) == local.path("file.tar.gz.other") assert p2.with_suffix(".other", None) == local.path("file.other") with pytest.raises(ValueError): p1.with_suffix("nodot") def test_newname(self): # This picks up the drive letter differently if not constructed here p1 = local.path("/some/long/path/to/file.txt") p2 = local.path("file.tar.gz") assert p1.with_name("something.tar") == local.path( "/some/long/path/to/something.tar" ) assert p2.with_name("something.tar") == local.path("something.tar") def test_relative_to(self): p = local.path("/var/log/messages") assert p.relative_to("/var/log/messages") == RelativePath([]) assert p.relative_to("/var/") == RelativePath(["log", "messages"]) assert p.relative_to("/") == RelativePath(["var", "log", "messages"]) assert p.relative_to("/var/tmp") == RelativePath(["..", "log", "messages"]) assert p.relative_to("/opt") == RelativePath(["..", "var", "log", "messages"]) assert p.relative_to("/opt/lib") == RelativePath( ["..", "..", "var", "log", "messages"] ) for src in [ local.path("/var/log/messages"), local.path("/var"), local.path("/opt/lib"), ]: delta = p.relative_to(src) assert src + delta == p def test_read_write(self): with local.tempdir() as dir: f = dir / "test.txt" text = b"hello world\xd7\xa9\xd7\x9c\xd7\x95\xd7\x9d".decode("utf8") f.write(text, "utf8") text2 = f.read("utf8") assert text == text2 def test_parts(self): parts = self.longpath.parts if IS_WIN32: assert parts[1:] == ("some", "long", "path", "to", "file.txt") assert ":" in parts[0] else: assert parts == ("/", "some", "long", "path", "to", "file.txt") @pytest.mark.usefixtures("testdir") def test_iterdir(self): cwd = local.path(".") files = list(cwd.iterdir()) assert cwd / "test_local.py" in files assert cwd / "test_remote.py" in files assert cwd["test_local.py"] in files assert cwd["test_remote.py"] in files def test_stem(self): assert self.longpath.stem == "file" p = local.path("/some/directory") assert p.stem == "directory" def test_root_drive(self): pathlib = pytest.importorskip("pathlib") pl_path = pathlib.Path("/some/long/path/to/file.txt").absolute() assert self.longpath.root == pl_path.root assert self.longpath.drive == pl_path.drive p_path = local.cwd / "somefile.txt" pl_path = pathlib.Path("somefile.txt").absolute() assert p_path.root == pl_path.root assert p_path.drive == pl_path.drive def test_compare_pathlib(self): pathlib = pytest.importorskip("pathlib") def filename_compare(name): p = local.path(str(name)) pl = pathlib.Path(str(name)).absolute() assert str(p) == str(pl) assert p.parts == pl.parts assert p.exists() == pl.exists() assert p.is_symlink() == pl.is_symlink() assert p.as_uri() == pl.as_uri() assert str(p.with_suffix(".this")) == str(pl.with_suffix(".this")) assert p.name == pl.name assert str(p.parent) == str(pl.parent) assert list(map(str, p.parents)) == list(map(str, pl.parents)) filename_compare("/some/long/path/to/file.txt") filename_compare(local.cwd / "somefile.txt") filename_compare("/some/long/path/") filename_compare("/some/long/path") filename_compare(__file__) def test_suffix_expected(self): assert self.longpath.preferred_suffix(".tar") == self.longpath assert (local.cwd / "this").preferred_suffix(".txt") == local.cwd / "this.txt" def test_touch(self): with local.tempdir() as tmp: one = tmp / "one" assert not one.is_file() one.touch() assert one.is_file() one.delete() assert not one.is_file() def test_copy_override(self): """Edit this when override behavior is added""" with local.tempdir() as tmp: one = tmp / "one" one.touch() two = tmp / "two" assert one.is_file() assert not two.is_file() one.copy(two) assert one.is_file() assert two.is_file() def test_copy_nonexistant_dir(self): with local.tempdir() as tmp: one = tmp / "one" one.write(b"lala") two = tmp / "two" / "one" three = tmp / "three" / "two" / "one" one.copy(two) assert one.read() == two.read() one.copy(three) assert one.read() == three.read() def test_unlink(self): with local.tempdir() as tmp: one = tmp / "one" one.touch() assert one.exists() one.unlink() assert not one.exists() def test_unhashable(self): with pytest.raises(TypeError): hash(local.cwd) def test_getpath(self): assert local.cwd.getpath() == local.path(".") def test_path_dir(self): assert local.path(__file__).dirname == SDIR def test_mkdir(self): # (identical to test_remote.TestRemotePath.test_mkdir) with local.tempdir() as tmp: tmp["a"].mkdir(exist_ok=False, parents=False) assert tmp["a"].exists() assert tmp["a"].is_dir() tmp["a"].mkdir(exist_ok=True, parents=False) tmp["a"].mkdir(exist_ok=True, parents=True) with pytest.raises(OSError): tmp["a"].mkdir(exist_ok=False, parents=False) with pytest.raises(OSError): tmp["a"].mkdir(exist_ok=False, parents=True) tmp["b"]["bb"].mkdir(exist_ok=False, parents=True) assert tmp["b"]["bb"].exists() assert tmp["b"]["bb"].is_dir() assert not tmp.exists() def test_mkdir_mode(self): # (identical to test_remote.TestRemotePath.test_mkdir_mode) with local.tempdir() as tmp: # just verify that mode argument works the same way it does for # Python's own os.mkdir, which takes into account the umask # (different from shell mkdir mode argument!); umask on my # system is 022 by default, so 033 is ok for testing this try: (tmp / "pb_333").mkdir(exist_ok=False, parents=False, mode=0o333) local.python( "-c", "import os; os.mkdir({}, 0o333)".format(repr(str(tmp / "py_333"))), ) pb_final_mode = oct((tmp / "pb_333").stat().st_mode) py_final_mode = oct((tmp / "py_333").stat().st_mode) assert pb_final_mode == py_final_mode finally: # we have to revert this so the tempdir deletion works if (tmp / "pb_333").exists(): (tmp / "pb_333").chmod(0o777) if (tmp / "py_333").exists(): (tmp / "py_333").chmod(0o777) assert not tmp.exists() def test_str_getitem(self): with local.tempdir() as tmp: assert str(tmp) == str(tmp[:]) assert str(tmp)[0] == str(tmp[0]) def test_fspath(self): with local.tempdir() as tmp: assert tmp.__fspath__() == str(tmp) @pytest.mark.usefixtures("testdir") class TestLocalMachine: def test_getattr(self): pb = plumbum assert getattr(pb.cmd, "does_not_exist", 1) == 1 ls_cmd1 = pb.cmd.non_exist1N9 if hasattr(pb.cmd, "non_exist1N9") else pb.cmd.ls ls_cmd2 = getattr(pb.cmd, "non_exist1N9", pb.cmd.ls) assert str(ls_cmd1) == str(local["ls"]) assert str(ls_cmd2) == str(local["ls"]) # TODO: This probably fails because of odd ls behavior @skip_on_windows def test_imports(self): from plumbum.cmd import ls assert "test_local.py" in local["ls"]().splitlines() assert "test_local.py" in ls().splitlines() with pytest.raises(CommandNotFound): local["non_exist1N9"]() with pytest.raises(ImportError): from plumbum.cmd import non_exist1N9 # noqa: F401 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")) assert str(local["ls"]) == str(local.get("non_exist1N9", "ls")) with pytest.raises(CommandNotFound): local.get("non_exist1N9") with pytest.raises(CommandNotFound): local.get("non_exist1N9", "non_exist1N8") with pytest.raises(CommandNotFound): local.get("non_exist1N9", "/tmp/non_exist1N8") def test_shadowed_by_dir(self): real_ls = local["ls"] 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) assert "FG" in repr(FG) @skip_on_windows def test_cwd(self): from plumbum.cmd import ls assert local.cwd == os.getcwd() assert "machines" not in ls().splitlines() with local.cwd("../plumbum"): assert "machines" in ls().splitlines() assert "machines" not in ls().splitlines() assert "machines" in ls.with_cwd("../plumbum")().splitlines() path = local.cmd.pwd.with_cwd("../plumbum")().strip() with local.cwd("/"): assert "machines" not in ls().splitlines() assert "machines" in ls.with_cwd(path)().splitlines() with pytest.raises(OSError): local.cwd.chdir("../non_exist1N9") @skip_on_windows def test_mixing_chdir(self): assert local.cwd == os.getcwd() os.chdir("../plumbum") assert local.cwd == os.getcwd() os.chdir("../tests") assert local.cwd == os.getcwd() def test_contains(self): assert "plumbum" in local.cwd / ".." assert "non_exist1N91" not in local.cwd / ".." @skip_on_windows def test_path(self): assert not (local.cwd / "../non_exist1N9").exists() assert (local.cwd / ".." / "plumbum").is_dir() # traversal found = False for fn in local.cwd / ".." / "plumbum": if fn.name == "__init__.py": assert fn.is_file() found = True assert found # glob'ing found = False for fn in local.cwd / ".." // "*/*.rst": if fn.name == "index.rst": found = True assert found for fn in local.cwd / ".." // ("*/*.rst", "*./*.html"): if fn.name == "index.rst": found = True assert found @skip_on_windows def test_glob_spaces(self): fileloc = local.cwd / "file with space.txt" assert fileloc.exists() assert local.cwd // "*space.txt" assert local.cwd // "file with*" @skip_on_windows def test_env(self): assert "PATH" in local.env assert "FOOBAR72" not in local.env with pytest.raises(ProcessExecutionError): local.python("-c", "import os;os.environ['FOOBAR72']") local.env["FOOBAR72"] = "spAm" assert local.python( "-c", "import os; print(os.environ['FOOBAR72'])" ).splitlines() == ["spAm"] with local.env(FOOBAR73=1889): assert local.python( "-c", "import os; print(os.environ['FOOBAR73'])" ).splitlines() == ["1889"] with local.env(FOOBAR73=1778): assert local.python( "-c", "import os; print(os.environ['FOOBAR73'])" ).splitlines() == ["1778"] assert local.python( "-c", "import os; print(os.environ['FOOBAR73'])" ).splitlines() == ["1889"] with pytest.raises(ProcessExecutionError): local.python("-c", "import os; os.environ['FOOBAR73']") # path manipulation with pytest.raises(CommandNotFound): local.which("dummy-executable") with local.env(): local.env.path.insert(0, local.cwd / "not-in-path") p = local.which("dummy-executable") assert p == local.cwd / "not-in-path" / "dummy-executable" def test_local(self): assert "plumbum" in str(local.cwd) assert "PATH" in local.env.getdict() assert local.path("foo") == os.path.join(os.getcwd(), "foo") local.which("ls") local["ls"] assert local.python("-c", "print('hi there')").splitlines() == ["hi there"] @skip_on_windows def test_piping(self): from plumbum.cmd import grep, ls chain = ls | grep["\\.py"] assert "test_local.py" in chain().splitlines() chain = ls["-a"] | grep["test"] | grep["local"] assert "test_local.py" in chain().splitlines() @skip_on_windows def test_redirection(self): from plumbum.cmd import cat, grep, ls, rm chain = (ls | grep["\\.py"]) > "tmp.txt" chain() chain2 = (cat < "tmp.txt") | grep["local"] assert "test_local.py" in chain2().splitlines() rm("tmp.txt") chain3 = ( cat << "this is the\nworld of helloness and\nspam bar and eggs" ) | grep["hello"] assert "world of helloness and" in chain3().splitlines() rc, _, err = (grep["-Zq5"] >= "tmp2.txt").run(["-Zq5"], retcode=None) assert rc == 2 assert not err assert "usage" in (cat < "tmp2.txt")().lower() rm("tmp2.txt") rc, out, _ = (grep["-Zq5"] >= ERROUT).run(["-Zq5"], retcode=None) assert rc == 2 assert "usage" in out.lower() @skip_on_windows def test_popen(self): from plumbum.cmd import ls p = ls.popen(["-a"]) out, _ = p.communicate() assert p.returncode == 0 assert "test_local.py" in out.decode(local.encoding).splitlines() def test_run(self): from plumbum.cmd import grep, ls rc, out, err = (ls | grep["non_exist1N9"]).run(retcode=1) assert rc == 1 def test_timeout(self): from plumbum.cmd import sleep with pytest.raises(ProcessTimedOut): sleep(3, timeout=1) @skip_on_windows def test_pipe_stderr(self, capfd): from plumbum.cmd import cat, head cat["/dev/urndom"] & FG(1) assert "urndom" in capfd.readouterr()[1] assert capfd.readouterr()[1] == "" (cat["/dev/urndom"] | head["-c", "10"]) & FG(retcode=1) assert "urndom" in capfd.readouterr()[1] @skip_on_windows def test_fair_error_attribution(self): # use LocalCommand directly for predictable argv false = LocalCommand("false") true = LocalCommand("true") with pytest.raises(ProcessExecutionError) as e: (false | true) & FG assert e.value.argv == ["false"] @skip_on_windows def test_iter_lines_timeout(self): from plumbum.cmd import bash cmd = bash["-ce", "for ((i=0;1==1;i++)); do echo $i; sleep .3; done"] with pytest.raises(ProcessTimedOut): # noqa: PT012 for i, (out, err) in enumerate(cmd.popen().iter_lines(timeout=1)): assert not err assert out print(i, "out:", out) assert i in (2, 3) # Mac is a bit flakey @skip_on_windows def test_iter_lines_buffer_size(self): 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: # 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" @skip_on_windows def test_iter_lines_timeout_by_type(self): from plumbum.cmd import bash from plumbum.commands.processes import BY_TYPE cmd = bash[ "-ce", "for ((i=0;1==1;i++)); do echo $i; sleep .3; echo $i 1>&2; done" ] types = {1: "out:", 2: "err:"} counts = {1: 0, 2: 0} 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 print(types[typ], line) assert counts[1] in (3, 4) # Mac is a bit flakey assert counts[2] in (2, 3) # Mac is a bit flakey @skip_on_windows def test_iter_lines_error(self): from plumbum.cmd import ls with pytest.raises(ProcessExecutionError) as err: # noqa: PT012 for i, _lines in enumerate(ls["--bla"].popen()): # noqa: B007 pass assert i == 1 assert ( "ls: unrecognized option" in err.value.stderr and "--bla" in err.value.stderr ) or "ls: illegal option -- -" in err.value.stderr @skip_on_windows def test_iter_lines_line_timeout(self): from plumbum.cmd import bash cmd = bash["-ce", "for ((i=0;1==1;i++)); do echo $i; sleep $i; done"] 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) print(i, "err:", err) assert i == 1 @skip_on_windows def test_modifiers(self): from plumbum.cmd import cat, grep, ls f = (ls["-a"] | grep["\\.py"]) & BG f.wait() assert "test_local.py" in f.stdout.splitlines() command = ls["-a"] | grep["local"] command_false = ls["-a"] | grep["not_a_file_here"] command_false_2 = command_false | cat command & FG assert command & TF assert not (command_false & TF) assert not (command_false_2 & TF) assert command & RETCODE == 0 assert command_false & RETCODE == 1 assert command_false_2 & RETCODE == 1 assert (command & TEE)[0] == 0 assert (command_false & TEE(retcode=None))[0] == 1 assert (command_false_2 & TEE(retcode=None))[0] == 1 @skip_on_windows def test_tee_modifier(self, capfd): from plumbum.cmd import echo result = echo["This is fun"] & TEE assert result[1] == "This is fun\n" assert capfd.readouterr()[0] == "This is fun\n" @skip_on_windows def test_tee_race(self, capfd): from plumbum.cmd import seq EXPECT = "".join(f"{i}\n" for i in range(1, 5001)) for _ in range(5): result = seq["1", "5000"] & TEE assert result[1] == EXPECT assert capfd.readouterr()[0] == EXPECT @skip_on_windows @pytest.mark.parametrize( ("modifier", "expected"), [ (FG, None), (TF(FG=True), True), (RETCODE(FG=True), 0), (TEE, (0, "meow", "")), ], ) def test_redirection_stdin_modifiers_fg(self, modifier, expected, capfd): "StdinDataRedirection compatible with modifiers which write to stdout" from plumbum.cmd import cat cmd = cat << "meow" assert cmd & modifier == expected assert capfd.readouterr() == ("meow", "") @skip_on_windows def test_logger_pipe(self): from plumbum.cmd import bash from plumbum.commands.modifiers import PipeToLoggerMixin logs = [] class Logger(PipeToLoggerMixin): def log(self, level, line): print(level, line) logs.append((level, line)) logger = Logger() ret = bash["-ce", "echo aaa"] & logger assert logs[-1] == (PipeToLoggerMixin.INFO, "aaa") assert ret == 0 bash["-ce", "echo bbb 1>&2"] & logger assert logs[-1] == (PipeToLoggerMixin.DEBUG, "bbb") ret = bash["-ce", "echo ccc 1>&2; false"] & logger.pipe( prefix="echo", retcode=1, err_level=0 ) assert logs[-1] == (0, "echo: ccc") assert ret == 1 @skip_on_windows def test_logger_pipe_line_timeout(self): from plumbum.cmd import bash from plumbum.commands.modifiers import PipeToLoggerMixin cmd = bash["-ce", "for ((i=0;i<10;i++)); do echo .$i; sleep .$i; done"] class Logger(PipeToLoggerMixin): def log(self, level, line): print(level, line) assert level == 20 assert float(line) <= 0.6 logger = Logger() with pytest.raises(ProcessLineTimedOut): # Order is important on mac cmd & logger.pipe(line_timeout=0.45) def test_arg_expansion(self): from plumbum.cmd import ls args = ["-l", "-F"] ls(*args) ls[args] @skip_on_windows def test_session(self): sh = local.session() for _ in range(4): _, out, _ = sh.run("ls -a") assert "test_local.py" in out.splitlines() sh.run("cd ..") sh.run("export FOO=17") out = sh.run("echo $FOO")[1] assert out.splitlines() == ["17"] def test_quoting(self): ssh = local["ssh"] pwd = local["pwd"] cmd = ssh[ "localhost", "cd", "/usr", "&&", ssh[ "localhost", "cd", "/", "&&", ssh["localhost", "cd", "/bin", "&&", pwd] ], ] assert "\"'&&'\"" in " ".join(cmd.formulate(0)) ls = local["ls"] with pytest.raises(ProcessExecutionError) as execinfo: ls("-a", "") # check that empty strings are rendered correctly assert execinfo.value.argv[-2:] == ["-a", ""] def test_exception_pickling(self): import pickle with pytest.raises(ProcessExecutionError) as exc_info: local.cmd.ls("no-file") assert pickle.loads(pickle.dumps(exc_info.value)).argv == exc_info.value.argv def test_tempdir(self): with local.tempdir() as dir: assert dir.is_dir() data = b"hello world" with open(str(dir / "test.txt"), "wb") as f: f.write(data) with open(str(dir / "test.txt"), "rb") as f: assert f.read() == data assert not dir.exists() def test_direct_open_tmpdir(self): with local.tempdir() as dir: assert dir.is_dir() data = b"hello world" with open(dir / "test.txt", "wb") as f: f.write(data) with open(dir / "test.txt", "rb") as f: assert f.read() == data assert not dir.exists() def test_read_write_str(self): with local.tempdir() as tmp: data = "hello world" (tmp / "foo.txt").write(data) assert (tmp / "foo.txt").read() == data def test_read_write_unicode(self): with local.tempdir() as tmp: data = "hello world" (tmp / "foo.txt").write(data) assert (tmp / "foo.txt").read() == data def test_read_write_bin(self): with local.tempdir() as tmp: data = b"hello world" (tmp / "foo.txt").write(data) assert (tmp / "foo.txt").read(mode="rb") == data def test_links(self): with local.tempdir() as tmp: src = tmp / "foo.txt" dst1 = tmp / "bar.txt" dst2 = tmp / "spam.txt" data = "hello world" src.write(data) src.link(dst1) assert data == dst1.read() src.symlink(dst2) assert data == dst2.read() def test_list_processes(self): assert list(local.list_processes()) def test_pgrep(self): assert list(local.pgrep("[pP]ython")) def _generate_sigint(self): with pytest.raises(KeyboardInterrupt): # noqa: PT012 if sys.platform == "win32": from win32api import GenerateConsoleCtrlEvent GenerateConsoleCtrlEvent(0, 0) # send Ctrl+C to current TTY else: os.kill(0, signal.SIGINT) time.sleep(1) @skip_without_tty @skip_on_windows def test_same_sesion(self): from plumbum.cmd import sleep p = sleep.popen([1000]) assert p.poll() is None self._generate_sigint() time.sleep(1) assert p.poll() is not None @skip_without_tty def test_new_session(self): from plumbum.cmd import sleep p = sleep.popen([1000], new_session=True) assert p.poll() is None self._generate_sigint() time.sleep(1) assert p.poll() is None p.terminate() # Hangs sometimes on Windows @skip_on_windows @pytest.mark.timeout(20) def test_local_daemon(self): from plumbum.cmd import sleep proc = local.daemonic_popen(sleep[5]) with pytest.raises(OSError): os.waitpid(proc.pid, 0) proc.wait() @skip_on_windows def test_atomic_file(self): af1 = AtomicFile("tmp.txt") af2 = AtomicFile("tmp.txt") af1.write_atomic(b"foo") af2.write_atomic(b"bar") assert af1.read_atomic() == b"bar" assert af2.read_atomic() == b"bar" local.path("tmp.txt").delete() @skip_on_windows def test_atomic_file2(self): af = AtomicFile("tmp.txt") code = """\ from plumbum.fs.atomic import AtomicFile af = AtomicFile("tmp.txt") try: with af.locked(blocking = False): raise ValueError("this should have failed") except (OSError, IOError): print("already locked") """ with af.locked(): output = local.python("-c", code) assert output.strip() == "already locked" local.path("tmp.txt").delete() @skip_on_windows def test_pid_file(self): code = """\ from plumbum.fs.atomic import PidFile, PidFileTaken try: with PidFile("mypid"): raise ValueError("this should have failed") except PidFileTaken: print("already locked") """ with PidFile("mypid"): output = local.python("-c", code) assert output.strip() == "already locked" local.path("mypid").delete() @skip_on_windows def test_atomic_counter(self): local.path("counter").delete() num_of_procs = 20 num_of_increments = 20 code = f"""from plumbum.fs.atomic import AtomicCounterFile import time time.sleep(0.2) afc = AtomicCounterFile.open("counter") for _ in range({num_of_increments}): print(afc.next()) time.sleep(0.1) """ procs = [] for _ in range(num_of_procs): procs.append(local.python["-c", code].popen()) results = [] for p in procs: out, _ = p.communicate() assert p.returncode == 0 results.extend(int(num) for num in out.splitlines()) assert len(results) == num_of_procs * num_of_increments assert len(set(results)) == len(results) assert min(results) == 0 assert max(results) == num_of_procs * num_of_increments - 1 local.path("counter").delete() @skip_on_windows def test_atomic_counter2(self): local.path("counter").delete() afc = AtomicCounterFile.open("counter") assert afc.next() == 0 assert afc.next() == 1 assert afc.next() == 2 with pytest.raises(TypeError): afc.reset("hello") afc.reset(70) assert afc.next() == 70 assert afc.next() == 71 assert afc.next() == 72 local.path("counter").delete() @skip_on_windows @pytest.mark.skipif("printenv" not in local, reason="printenv is missing") def test_bound_env(self): from plumbum.cmd import printenv with local.env(FOO="hello"): assert printenv.with_env(BAR="world")("FOO") == "hello\n" assert printenv.with_env(BAR="world")("BAR") == "world\n" assert printenv.with_env(FOO="sea", BAR="world")("FOO") == "sea\n" assert printenv("FOO") == "hello\n" assert local.cmd.pwd.with_cwd("/")() == "/\n" assert local.cmd.pwd["-L"].with_env(A="X").with_cwd("/")() == "/\n" def test_nesting_lists_as_argv(self): from plumbum.cmd import ls c = ls["-l", ["-a", "*.py"]] assert c.formulate()[1:] == ["-l", "-a", "*.py"] def test_contains_ls(self): assert "ls" in local def test_issue_139(self): LocalPath(local.cwd) def test_pipeline_failure(self): from plumbum.cmd import head, ls with pytest.raises(ProcessExecutionError): (ls["--no-such-option"] | head)() def test_cmd(self): local.cmd.ls("/tmp") def test_pipeline_retcode(self): "From PR #288" from plumbum.cmd import echo, grep print((echo["one two three four"] | grep["two"] | grep["three"])(retcode=None)) print((echo["one two three four"] | grep["five"] | grep["three"])(retcode=None)) print((echo["one two three four"] | grep["two"] | grep["five"])(retcode=None)) print((echo["one two three four"] | grep["six"] | grep["five"])(retcode=None)) def test_pipeline_stdin(self): from subprocess import PIPE from plumbum.cmd import cat with (cat | cat).bgrun(stdin=PIPE) as future: future.stdin.write(b"foobar") future.stdin.close() def test_run_bg(self): from plumbum.cmd import ls f = ls["-a"].run_bg() f.wait() assert "test_local.py" in f.stdout def test_run_fg(self, capfd): from plumbum.cmd import ls ls["-l"].run_fg() stdout = capfd.readouterr()[0] assert "test_local.py" in stdout @skip_on_windows def test_run_tee(self, capfd): from plumbum.cmd import echo result = echo["This is fun"].run_tee() assert result[1] == "This is fun\n" assert capfd.readouterr()[0] == "This is fun\n" def test_run_tf(self): from plumbum.cmd import ls f = ls["-l"].run_tf() assert f is True def test_run_retcode(self): from plumbum.cmd import ls f = ls["-l"].run_retcode() assert f == 0 def test_run_nohup(self): from plumbum.cmd import ls f = ls["-l"].run_nohup() f.wait() assert os.path.exists("nohup.out") os.unlink("nohup.out") class TestLocalEncoding: try: richstr = unichr(40960) except NameError: richstr = chr(40960) def test_inout_rich(self): from plumbum.cmd import echo out = echo(self.richstr) assert self.richstr in out @pytest.mark.usefixtures("cleandir") def test_out_rich(self): from plumbum.cmd import cat with open("temp.txt", "w", encoding="utf8") as f: f.write(self.richstr) out = cat("temp.txt") assert self.richstr in out @pytest.mark.xfail(IS_WIN32, reason="Unicode path not supported on Windows for now") @pytest.mark.usefixtures("cleandir") def test_runfile_rich(self): import os import stat name = self.richstr + "_program" with open(name, "w") as f: f.write(f"#!{sys.executable}\nprint('yes')") st = os.stat(name) os.chmod(name, st.st_mode | stat.S_IEXEC) assert "yes" in local[local.cwd / name]() @pytest.mark.skipif( IS_WIN32, reason="Windows does not support these weird paths, so unambiguous there" ) def test_local_glob_path(tmpdir): p = tmpdir.mkdir("a*b?c") p2 = tmpdir.mkdir("aanythingbxc") p2.join("something.txt").write("content") p.join("hello.txt").write("content") p.join("other.txt").write("content") pp = LocalPath(str(p)) assert len(pp // "*.txt") == 2 plumbum-1.8.3/tests/test_nohup.py0000644000000000000000000000422714613634536014056 0ustar00import os import time import psutil import pytest from plumbum import NOHUP, local try: from plumbum.cmd import bash, echo except ImportError: bash = None echo = None from plumbum._testtools import skip_on_windows from plumbum.path.utils import delete @skip_on_windows class TestNohupLocal: def read_file(self, filename): assert filename in os.listdir(".") with open(filename) as f: return f.read() @pytest.mark.usefixtures("testdir") def test_slow(self): delete("nohup.out") sp = bash["slow_process.bash"] sp & NOHUP time.sleep(0.5) assert self.read_file("slow_process.out") == "Starting test\n1\n" assert self.read_file("nohup.out") == "1\n" time.sleep(1) assert self.read_file("slow_process.out") == "Starting test\n1\n2\n" assert self.read_file("nohup.out") == "1\n2\n" time.sleep(2) delete("nohup.out", "slow_process.out") def test_append(self): delete("nohup.out") output = echo["This is output"] output & NOHUP time.sleep(0.2) assert self.read_file("nohup.out") == "This is output\n" output & NOHUP time.sleep(0.2) assert self.read_file("nohup.out") == "This is output\n" * 2 delete("nohup.out") def test_redir(self): delete("nohup_new.out") output = echo["This is output"] output & NOHUP(stdout="nohup_new.out") time.sleep(0.2) assert self.read_file("nohup_new.out") == "This is output\n" delete("nohup_new.out") (output > "nohup_new.out") & NOHUP time.sleep(0.2) assert self.read_file("nohup_new.out") == "This is output\n" delete("nohup_new.out") output & NOHUP time.sleep(0.2) assert self.read_file("nohup.out") == "This is output\n" delete("nohup.out") def test_closed_filehandles(self): proc = psutil.Process() file_handles_prior = proc.num_fds() sleep_proc = local["sleep"]["1"] & NOHUP sleep_proc.wait() file_handles_after = proc.num_fds() assert file_handles_prior >= file_handles_after plumbum-1.8.3/tests/test_pipelines.py0000644000000000000000000000473414613634536014720 0ustar00from typing import List, Tuple import pytest import plumbum from plumbum._testtools import skip_on_windows from plumbum.commands import BaseCommand @skip_on_windows @pytest.mark.timeout(3) def test_draining_stderr(generate_cmd, process_cmd): stdout, stderr = get_output_with_iter_lines( generate_cmd | process_cmd | process_cmd ) expected_output = {f"generated {i}" for i in range(5000)} expected_output.update(f"consumed {i}" for i in range(5000)) assert set(stderr) - expected_output == set() assert len(stderr) == 15000 assert len(stdout) == 5000 @skip_on_windows @pytest.mark.timeout(3) def test_draining_stderr_with_stderr_redirect(tmp_path, generate_cmd, process_cmd): stdout, stderr = get_output_with_iter_lines( generate_cmd | (process_cmd >= str(tmp_path / "output.txt")) | process_cmd ) expected_output = {f"generated {i}" for i in range(5000)} expected_output.update(f"consumed {i}" for i in range(5000)) assert set(stderr) - expected_output == set() assert len(stderr) == 10000 assert len(stdout) == 5000 @skip_on_windows @pytest.mark.timeout(3) def test_draining_stderr_with_stdout_redirect(tmp_path, generate_cmd, process_cmd): stdout, stderr = get_output_with_iter_lines( generate_cmd | process_cmd | process_cmd > str(tmp_path / "output.txt") ) expected_output = {f"generated {i}" for i in range(5000)} expected_output.update(f"consumed {i}" for i in range(5000)) assert set(stderr) - expected_output == set() assert len(stderr) == 15000 assert len(stdout) == 0 @pytest.fixture() def generate_cmd(tmp_path): generate = tmp_path / "generate.py" generate.write_text( """\ import sys for i in range(5000): print("generated", i, file=sys.stderr) print(i) """ ) return plumbum.local["python"][generate] @pytest.fixture() def process_cmd(tmp_path): process = tmp_path / "process.py" process.write_text( """\ import sys for line in sys.stdin: i = line.strip() print("consumed", i, file=sys.stderr) print(i) """ ) return plumbum.local["python"][process] def get_output_with_iter_lines(cmd: BaseCommand) -> Tuple[List[str], List[str]]: stderr, stdout = [], [] proc = cmd.popen() for stdout_line, stderr_line in proc.iter_lines(retcode=[0, None]): if stderr_line: stderr.append(stderr_line) if stdout_line: stdout.append(stdout_line) proc.wait() return stdout, stderr plumbum-1.8.3/tests/test_putty.py0000644000000000000000000000362214613634536014110 0ustar00"""Test that PuttyMachine initializes its SshMachine correctly""" import pytest from plumbum import PuttyMachine, SshMachine @pytest.fixture(params=["default", "322"]) def ssh_port(request): return request.param class TestPuttyMachine: def test_putty_command(self, mocker, ssh_port): local = mocker.patch("plumbum.machines.ssh_machine.local") init = mocker.spy(SshMachine, "__init__") mocker.patch("plumbum.machines.ssh_machine.BaseRemoteMachine") host = mocker.MagicMock() user = local.env.user port = keyfile = None ssh_command = local["plink"] scp_command = local["pscp"] ssh_opts = ["-ssh"] if ssh_port == "default": putty_port = None scp_opts = () else: putty_port = int(ssh_port) ssh_opts.extend(["-P", ssh_port]) scp_opts = ["-P", ssh_port] encoding = mocker.MagicMock() connect_timeout = 20 new_session = True PuttyMachine( host, port=putty_port, connect_timeout=connect_timeout, new_session=new_session, encoding=encoding, ) init.assert_called_with( mocker.ANY, host, user, port, keyfile=keyfile, ssh_command=ssh_command, scp_command=scp_command, ssh_opts=ssh_opts, scp_opts=scp_opts, encoding=encoding, connect_timeout=connect_timeout, new_session=new_session, ) def test_putty_str(self, mocker): local = mocker.patch("plumbum.machines.ssh_machine.local") mocker.patch("plumbum.machines.ssh_machine.BaseRemoteMachine") host = mocker.MagicMock() user = local.env.user machine = PuttyMachine(host) assert str(machine) == f"putty-ssh://{user}@{host}" plumbum-1.8.3/tests/test_remote.py0000644000000000000000000005623114613634536014222 0ustar00import logging import os import socket import time from multiprocessing import Queue from threading import Thread import env import pytest import plumbum from plumbum import ( NOHUP, CommandNotFound, ProcessExecutionError, ProcessTimedOut, RemotePath, SshMachine, local, ) from plumbum._testtools import skip_on_windows, skip_without_chown from plumbum.machines.session import HostPublicKeyUnknown, IncorrectLogin try: import paramiko except ImportError: paramiko = None else: from plumbum.machines.paramiko_machine import ParamikoMachine pytestmark = pytest.mark.ssh def strassert(one, two): assert str(one) == str(two) def assert_is_port(port): assert 0 < int(port) < 2**16 # TEST_HOST = "192.168.1.143" TEST_HOST = "127.0.0.1" if TEST_HOST not in ("::1", "127.0.0.1", "localhost"): plumbum.local.env.path.append("c:\\Program Files\\Git\\bin") @pytest.fixture(scope="session") def sshpass(): try: return plumbum.local["sshpass"] except CommandNotFound: pytest.skip("Test requires sshpass") @skip_on_windows def test_connection(): SshMachine(TEST_HOST) def test_incorrect_login(sshpass): # noqa: ARG001 with pytest.raises(IncorrectLogin): SshMachine( TEST_HOST, password="swordfish", ssh_opts=[ "-o", "PubkeyAuthentication=no", "-o", "PreferredAuthentications=password", ], ) @pytest.mark.xfail(env.LINUX, reason="TODO: no idea why this fails on linux") def test_hostpubkey_unknown(sshpass): # noqa: ARG001 with pytest.raises(HostPublicKeyUnknown): SshMachine( TEST_HOST, password="swordfish", ssh_opts=["-o", "UserKnownHostsFile=/dev/null", "-o", "UpdateHostKeys=no"], ) @skip_on_windows class TestRemotePath: def _connect(self): return SshMachine(TEST_HOST) def test_name(self): name = RemotePath(self._connect(), "/some/long/path/to/file.txt").name assert isinstance(name, str) 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 str(name) == "/some/long/path/to" def test_uri(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") 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") assert p.stem == "file" p = RemotePath(self._connect(), "/some/long/path/") assert p.stem == "path" def test_suffix(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") p2 = RemotePath(self._connect(), "file.tar.gz") assert p1.suffix == ".txt" assert p1.suffixes == [".txt"] assert p2.suffix == ".gz" assert p2.suffixes == [".tar", ".gz"] strassert( p1.with_suffix(".tar.gz"), RemotePath(self._connect(), "/some/long/path/to/file.tar.gz"), ) strassert( p2.with_suffix(".other"), RemotePath(self._connect(), "file.tar.other") ) strassert( p2.with_suffix(".other", 2), RemotePath(self._connect(), "file.other") ) strassert( p2.with_suffix(".other", 0), RemotePath(self._connect(), "file.tar.gz.other"), ) strassert( p2.with_suffix(".other", None), RemotePath(self._connect(), "file.other") ) def test_newname(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") p2 = RemotePath(self._connect(), "file.tar.gz") strassert( p1.with_name("something.tar"), RemotePath(self._connect(), "/some/long/path/to/something.tar"), ) strassert( p2.with_name("something.tar"), RemotePath(self._connect(), "something.tar") ) @skip_without_chown def test_chown(self): 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") p2 = p1.parent assert str(p2) == "/some/long/path/to" def test_mkdir(self): # (identical to test_local.TestLocalPath.test_mkdir) with self._connect() as rem: with rem.tempdir() as tmp: (tmp / "a").mkdir(exist_ok=False, parents=False) assert (tmp / "a").exists() assert (tmp / "a").is_dir() (tmp / "a").mkdir(exist_ok=True, parents=False) (tmp / "a").mkdir(exist_ok=True, parents=True) with pytest.raises(OSError): (tmp / "a").mkdir(exist_ok=False, parents=False) with pytest.raises(OSError): (tmp / "a").mkdir(exist_ok=False, parents=True) (tmp / "b" / "bb").mkdir(exist_ok=False, parents=True) assert (tmp / "b" / "bb").exists() assert (tmp / "b" / "bb").is_dir() assert not tmp.exists() @pytest.mark.xfail( reason="mkdir's mode argument is not yet implemented for remote paths", strict=True, ) def test_mkdir_mode(self): # (identical to test_local.TestLocalPath.test_mkdir_mode) with self._connect() as rem: with rem.tempdir() as tmp: # just verify that mode argument works the same way it does for # Python's own os.mkdir, which takes into account the umask # (different from shell mkdir mode argument!); umask on my # system is 022 by default, so 033 is ok for testing this try: (tmp / "pb_333").mkdir(exist_ok=False, parents=False, mode=0o333) rem.python( "-c", "import os; os.mkdir({}, 0o333)".format( repr(str(tmp / "py_333")) ), ) pb_final_mode = oct((tmp / "pb_333").stat().st_mode) py_final_mode = oct((tmp / "py_333").stat().st_mode) assert pb_final_mode == py_final_mode finally: # we have to revert this so the tempdir deletion works if (tmp / "pb_333").exists(): (tmp / "pb_333").chmod(0o777) if (tmp / "py_333").exists(): (tmp / "py_333").chmod(0o777) assert not tmp.exists() def test_copy(self): """ tests `RemotePath.copy` for the following scenarios: * copying a simple file from `file_a` to `copy_of_a` succeeds * copying file `file_a` into a directory `a_dir/copy_of_a` succeeds * copying a directory `a_dir` over an existing directory path with `override=False` fails * copying a directory `a_dir` over an existing directory path with `override=True` succeeds """ 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() assert (tmp / "file_a").is_file() # setup a directory for copying into... (tmp / "a_dir").mkdir(exist_ok=False, parents=False) assert (tmp / "a_dir").exists() assert (tmp / "a_dir").is_dir() # setup a 2nd directory for testing `override=False` (tmp / "b_dir").mkdir(exist_ok=False, parents=False) assert (tmp / "b_dir").exists() assert (tmp / "b_dir").is_dir() # copying a simple file (tmp / "file_a").copy(tmp / "copy_of_a") assert (tmp / "copy_of_a").exists() assert (tmp / "copy_of_a").is_file() # copying into a directory (tmp / "file_a").copy(tmp / "a_dir/copy_of_a") assert (tmp / "a_dir/copy_of_a").exists() assert (tmp / "a_dir/copy_of_a").is_file() # copying a directory on top of an existing directory using # `override=False` (should fail with TypeError) with pytest.raises(TypeError): (tmp / "a_dir").copy(tmp / "b_dir", override=False) # copying a directory on top of an existing directory using # `override=True` (should copy transparently) (tmp / "a_dir").copy(tmp / "b_dir", override=True) assert "copy_of_a" in (tmp / "b_dir") assert not tmp.exists() class BaseRemoteMachineTest: TUNNEL_PROG_AF_INET = r"""import sys, socket s = socket.socket() s.bind(("", 0)) s.listen(1) sys.stdout.write("{0}\n".format(s.getsockname()[1])) sys.stdout.flush() s2, _ = s.accept() data = s2.recv(100) s2.send(b"hello " + data) s2.close() s.close() """ TUNNEL_PROG_AF_UNIX = r"""import sys, socket, tempfile s = socket.socket(family=socket.AF_UNIX) socket_location = tempfile.NamedTemporaryFile() socket_location.close() s.bind(socket_location.name) s.listen(1) sys.stdout.write("{0}\n".format(s.getsockname())) sys.stdout.flush() s2, _ = s.accept() data = s2.recv(100) s2.send(b"hello " + data) s2.close() s.close() """ def test_basic(self): with self._connect() as rem: r_ssh = rem["ssh"] r_ls = rem["ls"] r_grep = rem["grep"] lines = r_ls("-a").splitlines() assert ".bashrc" in lines or ".bash_profile" in lines with rem.cwd(os.path.dirname(os.path.abspath(__file__))): cmd = r_ssh[ "localhost", "cd", rem.cwd, "&&", r_ls, "|", r_grep["\\.py"] ] assert "'|'" in str(cmd) assert "test_remote.py" in cmd() assert "test_remote.py" in [f.name for f in rem.cwd // "*.py"] # Testing for #271 def test_double_chdir(self): with self._connect() as rem: with rem.cwd(os.path.dirname(os.path.abspath(__file__))): rem["ls"]() with rem.cwd("/tmp"): rem["pwd"]() def test_glob(self): 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, 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 def test_cmd(self): with self._connect() as rem: rem.cmd.ls("/tmp") @pytest.mark.usefixtures("testdir") def test_download_upload(self): with self._connect() as rem: rem.upload("test_remote.py", "/tmp") r_ls = rem["ls"] r_rm = rem["rm"] assert "test_remote.py" in r_ls("/tmp").splitlines() rem.download("/tmp/test_remote.py", "/tmp/test_download.txt") r_rm("/tmp/test_remote.py") r_rm("/tmp/test_download.txt") def test_session(self): with self._connect() as rem: sh = rem.session() for _ in range(4): _, out, _ = sh.run("ls -a") assert ".bashrc" in out or ".bash_profile" in out @pytest.mark.xfail(env.PYPY, reason="PyPy sometimes fails here", strict=False) def test_env(self): with self._connect() as rem: with pytest.raises(ProcessExecutionError): rem.python("-c", "import os;os.environ['FOOBAR72']") with rem.env(FOOBAR72="lala"): with rem.env(FOOBAR72="baba"): out = rem.python("-c", "import os;print(os.environ['FOOBAR72'])") assert out.strip() == "baba" out = rem.python("-c", "import os;print(os.environ['FOOBAR72'])") assert out.strip() == "lala" # path manipulation with pytest.raises(CommandNotFound): rem.which("dummy-executable") with rem.cwd(os.path.dirname(os.path.abspath(__file__))): rem.env.path.insert(0, rem.cwd / "not-in-path") p = rem.which("dummy-executable") assert p == rem.cwd / "not-in-path" / "dummy-executable" @pytest.mark.xfail(env.PYPY, reason="PyPy sometimes fails here", strict=False) @pytest.mark.parametrize( "env", [ "lala", "-Wl,-O2 -Wl,--sort-common", "{{}}", "''", "!@%_-+=:", "'", "`", "$", "\\", ], ) def test_env_special_characters(self, env): with self._connect() as rem: with pytest.raises(ProcessExecutionError): rem.python("-c", "import os;print(os.environ['FOOBAR72'])") rem.env["FOOBAR72"] = env out = rem.python("-c", "import os;print(os.environ['FOOBAR72'])") assert out.strip() == env def test_read_write(self): with self._connect() as rem: with rem.tempdir() as dir: assert dir.is_dir() data = b"hello world" (dir / "foo.txt").write(data) assert (dir / "foo.txt").read() == data assert not dir.exists() def test_contains(self): with self._connect() as rem: assert "ls" in rem def test_iter_lines_timeout(self): with self._connect() as rem: try: for i, (out, err) in enumerate( # noqa: B007 rem["ping"]["-i", 0.5, "127.0.0.1"].popen().iter_lines(timeout=4) ): print("out:", out) print("err:", err) except NotImplementedError as err: pytest.skip(str(err)) except ProcessTimedOut: assert i > 3 else: pytest.fail("Expected a timeout") def test_iter_lines_error(self): with self._connect() as rem: with pytest.raises(ProcessExecutionError) as ex: # noqa: PT012 for i, _lines in enumerate(rem["ls"]["--bla"].popen()): # noqa: B007 pass assert i == 1 assert "ls: " in ex.value.stderr def test_touch(self): with self._connect() as rem: rfile = rem.cwd / "sillyfile" assert not rfile.exists() rfile.touch() assert rfile.exists() rfile.delete() def serve_reverse_tunnel(queue, port): s = socket.socket() s.bind(("", port)) s.listen(1) s2, _ = s.accept() data = s2.recv(100).decode("ascii").strip() queue.put(data) s2.close() s.close() @skip_on_windows class TestRemoteMachine(BaseRemoteMachineTest): def _connect(self): return SshMachine(TEST_HOST) @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() port_or_socket = p.stdout.readline().decode("ascii").strip() try: port_or_socket = int(port_or_socket) dhost = "localhost" except ValueError: dhost = None lport = 12222 if not dynamic_lport else 0 with rem.tunnel(lport, port_or_socket, dhost=dhost) as tun: if not dynamic_lport: assert tun.lport == lport else: assert_is_port(tun.lport) assert tun.dport == port_or_socket assert not tun.reverse s = socket.socket() s.connect(("localhost", tun.lport)) s.send(b"world") data = s.recv(100) s.close() print(p.communicate()) assert data == b"hello world" @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() tunnel_server = Thread(target=serve_reverse_tunnel, args=(queue, lport)) tunnel_server.start() message = str(time.time()) if not dynamic_dport: get_unbound_socket_remote = """import sys, socket s = socket.socket() s.bind(("", 0)) s.listen(1) sys.stdout.write(str(s.getsockname()[1])) sys.stdout.flush() s.close() """ p = (rem.python["-u"] << get_unbound_socket_remote).popen() remote_socket = p.stdout.readline().decode("ascii").strip() else: remote_socket = 0 with rem.tunnel( lport, remote_socket, dhost="localhost", reverse=True ) as tun: assert tun.lport == lport if not dynamic_dport: assert tun.dport == remote_socket else: assert_is_port(tun.dport) assert tun.reverse remote_send_af_inet = f"""import socket s = socket.socket() s.connect(("localhost", {tun.dport})) s.send("{message}".encode("ascii")) s.close() """ (rem.python["-u"] << remote_send_af_inet).popen() tunnel_server.join(timeout=1) assert queue.get() == message def test_get(self): with self._connect() as rem: assert str(rem["ls"]) == str(rem.get("ls")) assert str(rem["ls"]) == str(rem.get("not_a_valid_process_234", "ls")) assert "ls" in rem assert "not_a_valid_process_234" not in rem def test_list_processes(self): with self._connect() as rem: assert list(rem.list_processes()) def test_pgrep(self): with self._connect() as rem: assert list(rem.pgrep("ssh")) def test_nohup(self): with self._connect() as rem: sleep = rem["sleep"] sleep["5.793817"] & NOHUP(stdout=None, append=False) time.sleep(0.5) print(rem["ps"]("aux")) assert list(rem.pgrep("5.793817")) time.sleep(6) assert not list(rem.pgrep("5.793817")) def test_bound_env(self): with self._connect() as rem: printenv = rem["printenv"] with rem.env(FOO="hello"): assert printenv.with_env(BAR="world")("FOO") == "hello\n" assert printenv.with_env(BAR="world")("BAR") == "world\n" assert printenv.with_env(FOO="sea", BAR="world")("FOO") == "sea\n" assert printenv.with_env(FOO="sea", BAR="world")("BAR") == "world\n" assert rem.cmd.pwd.with_cwd("/")() == "/\n" assert rem.cmd.pwd["-L"].with_env(A="X").with_cwd("/")() == "/\n" @pytest.mark.skipif( "useradd" not in local, reason="System does not have useradd (Mac?)" ) def test_sshpass(self): with local.as_root(): local["useradd"]("-m", "-b", "/tmp", "testuser") try: with local.as_root(): try: (local["passwd"] << "123456")("--stdin", "testuser") except ProcessExecutionError: # some versions of passwd don't support --stdin, nothing to do in this case logging.warning("passwd failed") return with SshMachine("localhost", user="testuser", password="123456") as rem: assert rem["pwd"]().strip() == "/tmp/testuser" finally: with local.as_root(): local["userdel"]("-r", "testuser") @skip_on_windows class TestParamikoMachine(BaseRemoteMachineTest): def _connect(self): if paramiko is None: pytest.skip("System does not have paramiko installed") return ParamikoMachine(TEST_HOST, missing_host_policy=paramiko.AutoAddPolicy()) def test_tunnel(self): with self._connect() as rem: p = rem.python["-c", self.TUNNEL_PROG_AF_INET].popen() try: port = int(p.stdout.readline().strip()) except ValueError: print(p.communicate()) raise s = rem.connect_sock(port) s.send(b"world") data = s.recv(100) s.close() print(p.communicate()) assert data == b"hello world" def test_piping(self): with self._connect() as rem: try: rem["ls"] | rem["cat"] except NotImplementedError: pass else: pytest.fail("Should not pipe") @pytest.mark.xfail(message="Not working yet") def test_encoding(self): with self._connect() as rem: unicode_half = b"\xc2\xbd".decode("utf8") ret = rem["bash"]("-c", 'echo -e "\xc2\xbd"') assert ret == f"{unicode_half}\n" ret = list(rem["bash"]["-c", 'echo -e "\xc2\xbd"'].popen()) assert ret == [[f"{unicode_half}\n", None]] def test_path_open_remote_write_local_read(self): with self._connect() as rem: with rem.tempdir() as remote_tmpdir, local.tempdir() as tmpdir: assert remote_tmpdir.is_dir() assert tmpdir.is_dir() data = b"hello world" with (remote_tmpdir / "bar.txt").open("wb") as f: f.write(data) rem.download((remote_tmpdir / "bar.txt"), (tmpdir / "bar.txt")) assert (tmpdir / "bar.txt").open("rb").read() == data assert not remote_tmpdir.exists() assert not tmpdir.exists() def test_path_open_local_write_remote_read(self): with self._connect() as rem: with rem.tempdir() as remote_tmpdir, local.tempdir() as tmpdir: assert remote_tmpdir.is_dir() assert tmpdir.is_dir() data = b"hello world" with (tmpdir / "bar.txt").open("wb") as f: f.write(data) rem.upload((tmpdir / "bar.txt"), (remote_tmpdir / "bar.txt")) assert (remote_tmpdir / "bar.txt").open("rb").read() == data assert not remote_tmpdir.exists() assert not tmpdir.exists() plumbum-1.8.3/tests/test_sudo.py0000644000000000000000000000061214613634536013671 0ustar00import pytest from plumbum import local from plumbum._testtools import skip_on_windows pytestmark = pytest.mark.sudo # This is a separate file to make separating (ugly) sudo command easier # For example, you can now run test_local directly without typing a password class TestSudo: @skip_on_windows def test_as_user(self): with local.as_root(): local["date"]() plumbum-1.8.3/tests/test_terminal.py0000644000000000000000000001222214613634536014532 0ustar00import sys from collections import OrderedDict from contextlib import contextmanager from io import StringIO from plumbum.cli.terminal import Progress, ask, choose, hexdump, prompt @contextmanager def send_stdin(stdin="\n"): prevstdin = sys.stdin sys.stdin = StringIO(stdin) try: yield sys.stdin finally: sys.stdin = prevstdin class TestPrompt: def test_simple(self, capsys): with send_stdin("12"): assert prompt("Enter a random int:", type=int) == 12 assert capsys.readouterr()[0] == "Enter a random int: " def test_try_twice(self, capsys): with send_stdin("\n13"): assert prompt("Enter a random int:", type=int) == 13 assert capsys.readouterr()[0] == "Enter a random int: Enter a random int: " def test_str(self): with send_stdin("1234"): assert prompt("Enter a string", type=str) == "1234" def test_default(self, capsys): with send_stdin(""): assert prompt("Enter nothing", default="hi") == "hi" assert capsys.readouterr()[0] == "Enter nothing [hi]: " def test_typefail(self, capsys): with send_stdin("1.2\n13"): assert prompt("Enter int", type=int) == 13 assert "try again" in capsys.readouterr()[0] def test_validator(self, capsys): with send_stdin("12\n9"): assert ( prompt("Enter in range < 10", type=int, validator=lambda x: x < 10) == 9 ) assert "try again" in capsys.readouterr()[0] class TestTerminal: def test_ask(self, capsys): with send_stdin("\n"): assert ask("Do you like cats?", default=True) assert capsys.readouterr()[0] == "Do you like cats? [Y/n] " with send_stdin("\nyes"): assert ask("Do you like cats?") assert ( capsys.readouterr()[0] == "Do you like cats? (y/n) Invalid response, please try again\nDo you like cats? (y/n) " ) def test_choose(self, capsys): with send_stdin("foo\n2\n"): assert ( choose("What is your favorite color?", ["blue", "yellow", "green"]) == "yellow" ) assert ( capsys.readouterr()[0] == "What is your favorite color?\n(1) blue\n(2) yellow\n(3) green\nChoice: Invalid choice, please try again\nChoice: " ) with send_stdin("foo\n2\n"): assert ( choose( "What is your favorite color?", [("blue", 10), ("yellow", 11), ("green", 12)], ) == 11 ) assert ( capsys.readouterr()[0] == "What is your favorite color?\n(1) blue\n(2) yellow\n(3) green\nChoice: Invalid choice, please try again\nChoice: " ) with send_stdin("foo\n\n"): assert ( choose( "What is your favorite color?", ["blue", "yellow", "green"], default="yellow", ) == "yellow" ) assert ( capsys.readouterr()[0] == "What is your favorite color?\n(1) blue\n(2) yellow\n(3) green\nChoice [2]: Invalid choice, please try again\nChoice [2]: " ) def test_choose_dict(self): with send_stdin("23\n1"): value = choose("Pick", {"one": "a", "two": "b"}) assert value in ("a", "b") def test_ordered_dict(self): dic = OrderedDict() dic["one"] = "a" dic["two"] = "b" with send_stdin("1"): value = choose("Pick", dic) assert value == "a" with send_stdin("2"): value = choose("Pick", dic) assert value == "b" def test_choose_dict_default(self, capsys): dic = OrderedDict() dic["one"] = "a" dic["two"] = "b" with send_stdin(): assert choose("Pick", dic, default="a") == "a" assert "[1]" in capsys.readouterr()[0] def test_hexdump(self): data = "hello world my name is queen marry" + "A" * 66 + "foo bar" output = """\ 000000 | 68 65 6c 6c 6f 20 77 6f 72 6c 64 20 6d 79 20 6e | hello world my n 000010 | 61 6d 65 20 69 73 20 71 75 65 65 6e 20 6d 61 72 | ame is queen mar 000020 | 72 79 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | ryAAAAAAAAAAAAAA 000030 | 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA * 000060 | 41 41 41 41 66 6f 6f 20 62 61 72 | AAAAfoo bar""" assert "\n".join(hexdump(data)) == output assert "\n".join(hexdump(StringIO(data))) == output def test_progress(self, capsys): for _ in Progress.range(4, has_output=True, timer=False): print("hi") stdout, _stderr = capsys.readouterr() output = """\ 0% complete 0% complete hi 25% complete hi 50% complete hi 75% complete hi 100% complete """ assert stdout == output def test_progress_empty(self, capsys): for _ in Progress.range(0, has_output=True, timer=False): print("hi") stdout = capsys.readouterr().out output = "0/0 complete" assert output in stdout plumbum-1.8.3/tests/test_typed_env.py0000644000000000000000000000301014613634536014707 0ustar00import pytest from plumbum.typed_env import TypedEnv class TestTypedEnv: def test_env(self): class E(TypedEnv): terminal = TypedEnv.Str("TERM") B = TypedEnv.Bool("BOOL", default=True) I = TypedEnv.Int("INT INTEGER".split()) # noqa: E741 # noqa: E741 INTS = TypedEnv.CSV("CS_INTS", type=int) raw_env = {"TERM": "xterm", "CS_INTS": "1,2,3,4"} e = E(raw_env) assert e.terminal == "xterm" e.terminal = "foo" assert e.terminal == "foo" assert raw_env["TERM"] == "foo" assert "terminal" not in raw_env # check default assert e.B is True raw_env["BOOL"] = "no" assert e.B is False raw_env["BOOL"] = "0" assert e.B is False e.B = True assert raw_env["BOOL"] == "yes" e.B = False assert raw_env["BOOL"] == "no" assert [1, 2, 3, 4] == e.INTS e.INTS = [1, 2] assert [1, 2] == e.INTS e.INTS = [1, 2, 3, 4] with pytest.raises(KeyError): e.I # noqa: B018 raw_env["INTEGER"] = "4" assert e.I == 4 assert e["I"] == 4 e.I = "5" assert raw_env["INT"] == "5" assert e.I == 5 assert e["I"] == 5 assert "{I} {B} {terminal}".format(**e) == "5 False foo" 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" plumbum-1.8.3/tests/test_utils.py0000644000000000000000000000325214613634536014062 0ustar00import pytest from plumbum import SshMachine, local from plumbum._testtools import skip_on_windows from plumbum.path.utils import copy, delete, move @skip_on_windows @pytest.mark.ssh() def test_copy_move_delete(): from plumbum.cmd import touch with local.tempdir() as dir: (dir / "orog").mkdir() (dir / "orog" / "rec").mkdir() for i in range(20): touch(dir / "orog" / ("f%d.txt" % (i,))) for i in range(20, 40): touch(dir / "orog" / "rec" / ("f%d.txt" % (i,))) move(dir / "orog", dir / "orig") s1 = sorted(f.name for f in (dir / "orig").walk()) copy(dir / "orig", dir / "dup") s2 = sorted(f.name for f in (dir / "dup").walk()) assert s1 == s2 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 / "dup", dir / "dup2") s5 = sorted(f.name for f in (dir / "dup2").walk()) assert s1 == s5 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() s7 = sorted(f.name for f in (dir / "superdup").walk()) assert s1 == s7 # test rm delete(dir) plumbum-1.8.3/tests/test_validate.py0000644000000000000000000000510614613634536014513 0ustar00from plumbum import cli class TestValidator: def test_named(self): class Try: @cli.positional(x=abs, y=str) def main(selfy, x, y): pass assert Try.main.positional == [abs, str] assert Try.main.positional_varargs is None def test_position(self): class Try: @cli.positional(abs, str) def main(selfy, x, y): pass assert Try.main.positional == [abs, str] assert Try.main.positional_varargs is None def test_mix(self): class Try: @cli.positional(abs, str, d=bool) def main(selfy, x, y, z, d): pass assert Try.main.positional == [abs, str, None, bool] assert Try.main.positional_varargs is None def test_var(self): class Try: @cli.positional(abs, str, int) def main(selfy, x, y, *g): pass assert Try.main.positional == [abs, str] assert Try.main.positional_varargs is int def test_defaults(self): class Try: @cli.positional(abs, str) def main(selfy, x, y="hello"): pass assert Try.main.positional == [abs, str] class TestProg: def test_prog(self, capsys): class MainValidator(cli.Application): @cli.positional(int, int, int) def main(self, myint, myint2, *mylist): print(repr(myint), myint2, mylist) _, rc = MainValidator.run(["prog", "1", "2", "3", "4", "5"], exit=False) assert rc == 0 assert capsys.readouterr()[0].strip() == "1 2 (3, 4, 5)" def test_failure(self, capsys): class MainValidator(cli.Application): @cli.positional(int, int, int) def main(self, myint, myint2, *mylist): print(myint, myint2, mylist) _, rc = MainValidator.run(["prog", "1.2", "2", "3", "4", "5"], exit=False) assert rc == 2 value = capsys.readouterr()[0].strip() assert "int" in value assert "not" in value assert "1.2" in value def test_defaults(self, capsys): class MainValidator(cli.Application): @cli.positional(int, int) def main(self, myint, myint2=2): print(repr(myint), repr(myint2)) _, rc = MainValidator.run(["prog", "1"], exit=False) assert rc == 0 assert capsys.readouterr()[0].strip() == "1 2" _, rc = MainValidator.run(["prog", "1", "3"], exit=False) assert rc == 0 assert capsys.readouterr()[0].strip() == "1 3" plumbum-1.8.3/tests/test_visual_color.py0000644000000000000000000000435714613634536015432 0ustar00#!/usr/bin/env python3 import os import unittest from plumbum import colors # This is really intended to be run manually, so the output can be observed, rather than with py.test class TestVisualColor(unittest.TestCase): def setUp(self): if os.name == "nt": try: import colorama colorama.init() self.colorama = colorama colors.use_color = 1 print() print("Colorama initialized") except ImportError: self.colorama = None else: self.colorama = None def tearDown(self): if self.colorama: self.colorama.deinit() def testVisualColors(self): print() for c in colors.fg[:16]: with c: print("Cycle color test", end=" ") print(" - > back to normal") with colors: print( colors.fg.green + "Green " + colors.bold + "Bold " + ~colors.bold + "Normal" ) print("Reset all") def testToggleColors(self): print() print(colors.fg.red["This is in red"], "but this is not") print( colors.fg.green + "Hi, " + colors.bg[23] + "This is on a BG" + ~colors.bg + " and this is not but is still green." ) colors.yellow.print("This is printed from color.") colors.reset() for attr in colors._style.attribute_names: print("This is", attr | getattr(colors, attr), "and this is not.") colors.reset() def testLimits(self): print() cval = colors.use_color colors.use_color = 4 c = colors.rgb(123, 40, 200) print("True", repr(str(c)), repr(c)) colors.use_color = 3 print("Full", repr(str(c)), repr(c)) colors.use_color = 2 print("Simple", repr(str(c)), repr(c)) colors.use_color = 1 print("Basic", repr(str(c)), repr(c)) colors.use_color = 0 print("None", repr(str(c)), repr(c)) colors.use_color = cval if __name__ == "__main__": unittest.main() plumbum-1.8.3/tests/not-in-path/dummy-executable0000755000000000000000000000001214613634536016636 0ustar00#!/bin/sh plumbum-1.8.3/.gitignore0000644000000000000000000000616714613634536012147 0ustar00# 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 plumbum-1.8.3/LICENSE0000644000000000000000000000207014613634536011151 0ustar00Copyright (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. plumbum-1.8.3/README.rst0000644000000000000000000001541014613634536011635 0ustar00.. 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 `_ 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 >>> 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" 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 `_-compatible clients, `PuTTY `_ (on Windows) and `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.") plumbum-1.8.3/pyproject.toml0000644000000000000000000001333614613634536013067 0ustar00[build-system] requires = [ "hatchling", "hatch-vcs", ] build-backend = "hatchling.build" [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] files = ["plumbum"] python_version = "3.8" warn_unused_configs = true warn_unused_ignores = true show_error_codes = true enable_error_code = ["ignore-without-code", "truthy-bool"] disallow_any_generics = false disallow_subclassing_any = false disallow_untyped_calls = false disallow_untyped_defs = false disallow_incomplete_defs = true check_untyped_defs = false disallow_untyped_decorators = false no_implicit_optional = true warn_redundant_casts = true 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 [tool.pytest.ini_options] testpaths = ["tests"] minversion = "6.0" addopts = ["-ra", "--showlocals", "--cov-config=setup.cfg", "--strict-markers", "--strict-config"] norecursedirs = ["examples", "experiments"] filterwarnings = [ "always" ] log_cli_level = "info" xfail_strict = true required_plugins = ["pytest-timeout", "pytest-mock"] timeout = 300 optional_tests = """ ssh: requires self ssh access to run sudo: requires sudo access to run """ [tool.pylint] master.py-version = "3.6" master.jobs = "0" master.load-plugins = ["pylint.extensions.no_self_use"] reports.output-format = "colorized" similarities.ignore-imports = "yes" messages_control.enable = [ "useless-suppression", ] messages_control.disable = [ "arguments-differ", # TODO: investigate "attribute-defined-outside-init", # TODO: investigate "broad-except", # TODO: investigate "consider-using-with", # TODO: should be handled "cyclic-import", "duplicate-code", # TODO: check "fixme", "import-error", "import-outside-toplevel", # TODO: see if this can be limited to certain imports "invalid-name", "line-too-long", "missing-class-docstring", "missing-function-docstring", "missing-module-docstring", "no-member", #"non-parent-init-called", # TODO: should be looked at "protected-access", "too-few-public-methods", "too-many-arguments", "too-many-branches", "too-many-function-args", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-nested-blocks", "too-many-public-methods", "too-many-return-statements", "too-many-statements", "unidiomatic-typecheck", # TODO: might be able to remove "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] target-version = "py37" exclude = ["docs/conf.py"] [tool.ruff.lint] extend-select = [ "B", # 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 ] ignore = [ "E501", "PLR", "PT004", "PT011", # TODO: add match parameter "RUF012", # ClassVar required if mutable "ISC001", # conflicts with formatter ] flake8-unused-arguments.ignore-variadic-names = true [tool.ruff.lint.per-file-ignores] "examples/*" = ["T20"] "experiments/*" = ["T20"] "tests/*" = ["T20"] "plumbum/cli/application.py" = ["T20"] "plumbum/commands/base.py" = ["SIM115"] "plumbum/commands/daemons.py" = ["SIM115"] [tool.codespell] ignore-words-list = "ans,switchs,hart,ot,twoo,fo" skip = "*.po" plumbum-1.8.3/PKG-INFO0000644000000000000000000002362414613634536011251 0ustar00Metadata-Version: 2.3 Name: plumbum Version: 1.8.3 Summary: Plumbum: shell combinators library 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: Cheatsheet, https://plumbum.readthedocs.io/en/latest/quickref.html Author-email: Tomer Filiba 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 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: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Requires-Python: >=3.6 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 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/ :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 `_ 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 >>> 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" 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 `_-compatible clients, `PuTTY `_ (on Windows) and `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.")