pax_global_header00006660000000000000000000000064144354573260014527gustar00rootroot0000000000000052 comment=01d5d27f64aeed8d298f6089fd1a49371ad96852 docopt-ng-0.9.0/000077500000000000000000000000001443545732600134275ustar00rootroot00000000000000docopt-ng-0.9.0/.github/000077500000000000000000000000001443545732600147675ustar00rootroot00000000000000docopt-ng-0.9.0/.github/workflows/000077500000000000000000000000001443545732600170245ustar00rootroot00000000000000docopt-ng-0.9.0/.github/workflows/test.yml000066400000000000000000000035231443545732600205310ustar00rootroot00000000000000name: Test on: pull_request: push: branches: - master tags: - "*" jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 5 matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up PDM for Python ${{ matrix.python-version }} uses: pdm-project/setup-pdm@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pdm sync -d -G dev # Linting is taken care of by pre-commit # in a different github action - name: Run tests run: | pdm run -v pytest --cov-report term-missing --cov-report xml --cov=docopt --mypy - name: Upload coverage uses: codecov/codecov-action@v2 with: name: Python ${{ matrix.python-version }} fail_ci_if_error: true - name: Build wheels and sdist run: | pdm build - name: Save artifacts uses: actions/upload-artifact@v2 with: # These are pure-python, so we don't need to worry about platform name: wheels # name: Python ${{ matrix.python-version }} path: | dist/*.whl dist/*.tar.gz release: needs: - test runs-on: ubuntu-latest steps: - name: Download artifacts uses: actions/download-artifact@v2 with: name: wheels path: dist - name: Upload packages to Jazzband if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} repository_url: https://jazzband.co/projects/docopt-ng/upload docopt-ng-0.9.0/.gitignore000066400000000000000000000004061443545732600154170ustar00rootroot00000000000000.* !.github !.gitignore !.pre-commit-config.yaml *.py[co] # Vim *.swp # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage coverage.xml .pdm-python docopt-ng-0.9.0/.pre-commit-config.yaml000066400000000000000000000003001443545732600177010ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.270 hooks: - id: ruff docopt-ng-0.9.0/CHANGELOG.md000066400000000000000000000127071443545732600152470ustar00rootroot00000000000000# CHANGELOG ## UNRELEASED ## Version 0.9.0: ### Changed - BREAKING: Remove `magic` stuff. When using docopt(): Now you must supply `docstring` explicitly, and the `more_magic` option is removed. The `magic()` and `magic_docopt()` functions are also removed. I had several reasons for removing this: 1. It's not needed. In 99% of cases you can just supply __doc__. 2. It is implicit and too magical, encouraging code that is hard to reason about. 3. It's brittle. See https://github.com/jazzband/docopt-ng/issues/49 4. It is different from the spec outlined on docopt.org. I want them to be more aligned, because it isn't obvious to users that these two might be out of sync. (no one seems to have control of that documentation site) 5. It fills in args in the calling scope???! We just returned the parsed result, just set it manually! 6. It should be easy to migrate to this new version, and I don't think I've broken many people. 7. It is out of scope. This library does one thing really well, and that is parsing the docstring. You can use the old code as an example if you want to re-create the magic. - Tweak a few things to restore compatibility with docopt (the original repo) 0.6.2 See PR https://github.com/jazzband/docopt-ng/pull/36 for more info 1. BREAKING: Restore what constitutes an "option": Now the important rule to follow is `any line starting with - or -- is treated as an option`. This means that some things that did NOT used to be treated as options, now ARE treated as options: 1. lines before `usage:` 2. non-indented --options 3. lines not inside the options: section However, we also keep one part of the old behavior of this fork where in the line `header that ends with the keyword options: --foo`, --foo is still treated as an option because the start of a line up to `options:` is ignored. 2. BREAKING: Error messages are tweaked a little bit. Unlikely that you relied on them, but just in case. 3. NONBREAKING: Now allow for blank lines between options. As described in https://github.com/jazzband/docopt-ng/issues/33 4. NONBREAKING: Fix an unlikely edge case of how options are parsed: Here, --foo was interpreted to take "Enable" as an argument ``` options: --foo Enable the foo behaviour. (One space before "Enable") ``` Whereas here, Enable was interpreted as part of the description. ``` options: --foo Enable the foo behaviour. (2 space before "Enable".) ``` Now, both of these examples are treated more intuitively, where Enable is treated as the description - (for devs) Switch to PDM as project manager ## Version 0.8.1: - Fixup of auto release in Github Actions ## Version 0.8.0: - Expose `DocoptExit` in `__all__`, let users to raise DocoptExit from their code https://github.com/jazzband/docopt-ng/pull/8 - Fix magic with arguments that have a dash https://github.com/jazzband/docopt-ng/pull/6 - Add `py.typed` so now users can actuall use mypy with us! https://github.com/jazzband/docopt-ng/commit/de7c861dafb86418da423d4829f389a62c82151a - Migrate to being maintained by Jazzband! - Migrate to GitHub actions from TravisCI - Update and tweak many little things in build, testing, and docs ## Version 0.7.2: - Complete MyPy typehints - ZERO errors. Required refactoring class implementations, adding typing stubs, but not changing tests. :) - 100% code coverage. Required the addition of a few tests. Removed unused codepaths. Tagged typing stubs `pragma: no cover` as they are definitionally exercised. ## Version 0.7.1: - Add `magic()` and `magic_docopt()` aliases for `docopt()` allowing easier use of new features. ## Version 0.7.0: - "MORE MAGIC" - First argument is now optional - `docopt()` will look for `__doc__` defined in parent scopes. - Dot access is supported on resulting `arguments` object, ignoring angle brackets and leading dashes. - `more_magic` parameter added to `docopt()` defaults False. - If `more_magic` enabled, `arguments` variable created and populated in calling scope with results. - If `more_magic` enabled, fuzzy (levenshtein) autocorrect enabled for long-args. - Lots of typehints. - README moved to Markdown. ## Version 0.6.3: - Catch up on \~two years of pull requests. - Fork [docopt](https://github.com/docopt/docopt) to [docopt-ng](https://github.com/bazaar-projects/docopt-ng). - Add levenshtein based autocorrect from [string-dist](https://github.com/obulkin/string-dist). - Add better debug / error messages. - Linting (via [black](https://github.com/ambv/black) and [flake8](https://gitlab.com/pycqa/flake8)). ## Version 0.6.2: - Bugfixes ## Version 0.6.1: - Fix issue [\#85](https://github.com/docopt/docopt/issues/85) which caused improper handling of `[options]` shortcut if it was present several times. ## Version 0.6.0: - New argument `options_first`, disallows interspersing options and arguments. If you supply `options_first=True` to `docopt`, it will interpret all arguments as positional arguments after first positional argument. - If option with argument could be repeated, its default value will be interpreted as space-separated list. E.g. with `[default: ./here ./there]` will be interpreted as `['./here', './there']`. docopt-ng-0.9.0/CODE_OF_CONDUCT.md000066400000000000000000000045071443545732600162340ustar00rootroot00000000000000# Code of Conduct As contributors and maintainers of the Jazzband projects, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in the Jazzband a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery - Personal attacks - Trolling or insulting/derogatory comments - Public or private harassment - Publishing other's private information, such as physical or electronic addresses, without explicit permission - Other unethical or unprofessional conduct The Jazzband roadies have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, the roadies commit themselves to fairly and consistently applying these principles to every aspect of managing the jazzband projects. Roadies who do not follow or enforce the Code of Conduct may be permanently removed from the Jazzband roadies. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Roadies are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/3/0/ docopt-ng-0.9.0/CONTRIBUTING.md000066400000000000000000000004641443545732600156640ustar00rootroot00000000000000[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). docopt-ng-0.9.0/LICENSE-MIT000066400000000000000000000020761443545732600150700ustar00rootroot00000000000000Copyright (c) 2012 Vladimir Keleshev, 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. docopt-ng-0.9.0/README.md000066400000000000000000000310021443545732600147020ustar00rootroot00000000000000# **docopt-ng** creates *beautiful* command-line interfaces [![Test](https://github.com/jazzband/docopt-ng/actions/workflows/test.yml/badge.svg?event=push)](https://github.com/jazzband/docopt-ng/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/jazzband/docopt-ng/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/docopt-ng) [![image](https://img.shields.io/pypi/v/docopt-ng.svg)](https://pypi.python.org/pypi/docopt-ng) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) **docopt-ng** is a fork of the [original docopt](https://github.com/docopt/docopt), now maintained by the [jazzband](https://jazzband.co/) project. Now with maintenance, typehints, and complete test coverage! **docopt-ng** helps you create beautiful command-line interfaces: ```python """Naval Fate. Usage: naval_fate.py ship new ... naval_fate.py ship move [--speed=] naval_fate.py ship shoot naval_fate.py mine (set|remove) [--moored | --drifting] naval_fate.py (-h | --help) naval_fate.py --version Options: -h --help Show this screen. --version Show version. --speed= Speed in knots [default: 10]. --moored Moored (anchored) mine. --drifting Drifting mine. """ from docopt import docopt if __name__ == "__main__": argv = ["ship", "Guardian", "move", "100", "150", "--speed=15"] arguments = docopt(__doc__, argv) print(arguments) ``` results in: ```python {'--drifting': False, '--help': False, '--moored': False, '--speed': '15', '--version': False, '': ['Guardian'], '': '100', '': '150', 'mine': False, 'move': True, 'new': False, 'remove': False, 'set': False, 'ship': True, 'shoot': False} ``` Beat that! The option parser is generated based on the docstring above that is passed to `docopt` function. `docopt` parses the usage pattern (`"Usage: ..."`) and option descriptions (lines starting with dash "`-`") and ensures that the program invocation matches the usage pattern; it parses options, arguments and commands based on that. The basic idea is that *a good help message has all necessary information in it to make a parser*. Also, [PEP 257](http://www.python.org/dev/peps/pep-0257/) recommends putting help message in the module docstrings. # Installation Use [pip](http://pip-installer.org): python -m pip install docopt-ng **docopt-ng** is tested with Python 3.7+. # API ```python def docopt( docstring: str, argv: list[str] | str | None = None, default_help: bool = True, version: Any = None, options_first: bool = False, ) -> ParsedOptions: ``` `docopt` takes a docstring, and 4 optional arguments: - `docstring` is a string that contains a **help message** that will be used to create the option parser. The simple rules of how to write such a help message are given in next sections. Typically you would just use `__doc__`. - `argv` is an optional argument vector; by default `docopt` uses the argument vector passed to your program (`sys.argv[1:]`). Alternatively you can supply a list of strings like `["--verbose", "-o", "hai.txt"]`, or a single string that will be split on spaces like `"--verbose -o hai.txt"`. - `default_help`, by default `True`, specifies whether the parser should automatically print the help message (supplied as `doc`) and terminate, in case `-h` or `--help` option is encountered (options should exist in usage pattern, more on that below). If you want to handle `-h` or `--help` options manually (as other options), set `help=False`. - `version`, by default `None`, is an optional argument that specifies the version of your program. If supplied, then, (assuming `--version` option is mentioned in usage pattern) when parser encounters the `--version` option, it will print the supplied version and terminate. `version` could be any printable object, but most likely a string, e.g. `"2.1.0rc1"`. > Note, when `docopt` is set to automatically handle `-h`, `--help` > and `--version` options, you still need to mention them in usage > pattern for this to work. Also, for your users to know about them. - `options_first`, by default `False`. If set to `True` will disallow mixing options and positional argument. I.e. after first positional argument, all arguments will be interpreted as positional even if the look like options. This can be used for strict compatibility with POSIX, or if you want to dispatch your arguments to other programs. The **return** value is a simple dictionary with options, arguments and commands as keys, spelled exactly like in your help message. Long versions of options are given priority. Furthermore, dot notation is supported, with preceeding dashes (`-`) and surrounding brackets (`<>`) ignored, for example `arguments.drifting` or `arguments.x`. # Help message format Help message consists of 2 parts: - Usage pattern, e.g.: Usage: my_program.py [-hso FILE] [--quiet | --verbose] [INPUT ...] - Option descriptions, e.g.: -h --help show this -s --sorted sorted output -o FILE specify output file [default: ./test.txt] --quiet print less text --verbose print more text Their format is described below; other text is ignored. ## Usage pattern format **Usage pattern** is a substring of `doc` that starts with `usage:` (case *insensitive*) and ends with a *visibly* empty line. Minimum example: ```python """Usage: my_program.py """ ``` The first word after `usage:` is interpreted as your program's name. You can specify your program's name several times to signify several exclusive patterns: ```python """Usage: my_program.py FILE my_program.py COUNT FILE """ ``` Each pattern can consist of the following elements: - **<arguments>**, **ARGUMENTS**. Arguments are specified as either upper-case words, e.g. `my_program.py CONTENT-PATH` or words surrounded by angular brackets: `my_program.py `. - **--options**. Options are words started with dash (`-`), e.g. `--output`, `-o`. You can "stack" several of one-letter options, e.g. `-oiv` which will be the same as `-o -i -v`. The options can have arguments, e.g. `--input=FILE` or `-i FILE` or even `-iFILE`. However it is important that you specify option descriptions if you want your option to have an argument, a default value, or specify synonymous short/long versions of the option (see next section on option descriptions). - **commands** are words that do *not* follow the described above conventions of `--options` or `` or `ARGUMENTS`, plus two special commands: dash "`-`" and double dash "`--`" (see below). Use the following constructs to specify patterns: - **\[ \]** (brackets) **optional** elements. e.g.: `my_program.py [-hvqo FILE]` - **( )** (parens) **required** elements. All elements that are *not* put in **\[ \]** are also required, e.g.: `my_program.py --path= ...` is the same as `my_program.py (--path= ...)`. (Note, "required options" might be not a good idea for your users). - **|** (pipe) **mutually exclusive** elements. Group them using **( )** if one of the mutually exclusive elements is required: `my_program.py (--clockwise | --counter-clockwise) TIME`. Group them using **\[ \]** if none of the mutually-exclusive elements are required: `my_program.py [--left | --right]`. - **...** (ellipsis) **one or more** elements. To specify that arbitrary number of repeating elements could be accepted, use ellipsis (`...`), e.g. `my_program.py FILE ...` means one or more `FILE`-s are accepted. If you want to accept zero or more elements, use brackets, e.g.: `my_program.py [FILE ...]`. Ellipsis works as a unary operator on the expression to the left. - **\[options\]** (case sensitive) shortcut for any options. You can use it if you want to specify that the usage pattern could be provided with any options defined below in the option-descriptions and do not want to enumerate them all in usage-pattern. - "`[--]`". Double dash "`--`" is used by convention to separate positional arguments that can be mistaken for options. In order to support this convention add "`[--]`" to your usage patterns. - "`[-]`". Single dash "`-`" is used by convention to signify that `stdin` is used instead of a file. To support this add "`[-]`" to your usage patterns. "`-`" acts as a normal command. If your pattern allows to match argument-less option (a flag) several times: Usage: my_program.py [-v | -vv | -vvv] then number of occurrences of the option will be counted. I.e. `args["-v"]` will be `2` if program was invoked as `my_program -vv`. Same works for commands. If your usage patterns allows to match same-named option with argument or positional argument several times, the matched arguments will be collected into a list: Usage: my_program.py --path=... I.e. invoked with `my_program.py file1 file2 --path=./here --path=./there` the returned dict will contain `args[""] == ["file1", "file2"]` and `args["--path"] == ["./here", "./there"]`. ## Option descriptions format **Option descriptions** consist of a list of options that you put below your usage patterns. It is necessary to list option descriptions in order to specify: - synonymous short and long options, - if an option has an argument, - if option's argument has a default value. The rules are as follows: - Every line in `doc` that starts with `-` or `--` (not counting spaces) is treated as an option description, e.g.: Options: --verbose # GOOD -o FILE # GOOD Other: --bad # BAD, line does not start with dash "-" - To specify that option has an argument, put a word describing that argument after space (or equals "`=`" sign) as shown below. Follow either <angular-brackets> or UPPER-CASE convention for options' arguments. You can use comma if you want to separate options. In the example below, both lines are valid, however you are recommended to stick to a single style.: -o FILE --output=FILE # without comma, with "=" sign -i , --input # with comma, without "=" sign - Use two spaces to separate options with their informal description: --verbose More text. # BAD, will be treated as if verbose option had # an argument "More", so use 2 spaces instead -q Quit. # GOOD -o FILE Output file. # GOOD --stdout Use stdout. # GOOD, 2 spaces - If you want to set a default value for an option with an argument, put it into the option-description, in form `[default: ]`: --coefficient=K The K coefficient [default: 2.95] --output=FILE Output file [default: test.txt] --directory=DIR Some directory [default: ./] - If the option is not repeatable, the value inside `[default: ...]` will be interpreted as string. If it *is* repeatable, it will be splited into a list on whitespace: Usage: my_program.py [--repeatable= --repeatable=] [--another-repeatable=]... [--not-repeatable=] # will be ["./here", "./there"] --repeatable= [default: ./here ./there] # will be ["./here"] --another-repeatable= [default: ./here] # will be "./here ./there", because it is not repeatable --not-repeatable= [default: ./here ./there] # Examples We have an extensive list of [examples](https://github.com/jazzband/docopt-ng/tree/master/examples) which cover every aspect of functionality of **docopt-ng**. Try them out, read the source if in doubt. # Development We would *love* to hear what you think about **docopt-ng** on our [issues page](https://github.com/jazzband/docopt-ng/issues). Make pull requests, report bugs, and suggest ideas. To setup your dev environment, fork this repo and clone it locally. We use [pdm](https://pdm.fming.dev/latest/#installation) to manage the project, so install that first. Then install dev requirements and the package itself as editable, then install the pre-commit hooks: pdm sync -d -G dev pdm run pre-commit install Useful testing, linting, and formatting commands: pdm run pytest pdm run black . pdm run ruff . docopt-ng-0.9.0/docopt/000077500000000000000000000000001443545732600147175ustar00rootroot00000000000000docopt-ng-0.9.0/docopt/__init__.py000066400000000000000000001030351443545732600170320ustar00rootroot00000000000000"""Docopt is a Pythonic command-line interface parser that will make you smile. Now: with spellcheck, flag extension (de-abbreviation), and capitalization fixes. (but only when unambiguous) * Licensed under terms of MIT license (see LICENSE-MIT) Contributors (roughly in chronological order): * Copyright (c) 2012 Andrew Kassen * Copyright (c) 2012 jeffrimko * Copyright (c) 2012 Andrew Sutton * Copyright (c) 2012 Andrew Sutton * Copyright (c) 2012 Nima Johari * Copyright (c) 2012-2013 Vladimir Keleshev, vladimir@keleshev.com * Copyright (c) 2014-2018 Matt Boersma * Copyright (c) 2016 amir * Copyright (c) 2015 Benjamin Bach * Copyright (c) 2017 Oleg Bulkin * Copyright (c) 2018 Iain Barnett * Copyright (c) 2019 itdaniher, itdaniher@gmail.com """ from __future__ import annotations import re import sys from typing import Any from typing import Callable from typing import NamedTuple from typing import Tuple from typing import Type from typing import Union from typing import cast from ._version import __version__ as __version__ __all__ = ["docopt", "DocoptExit"] def levenshtein_norm(source: str, target: str) -> float: """Calculates the normalized Levenshtein distance between two string arguments. The result will be a float in the range [0.0, 1.0], with 1.0 signifying the biggest possible distance between strings with these lengths """ # Compute Levenshtein distance using helper function. The max is always # just the length of the longer string, so this is used to normalize result # before returning it distance = levenshtein(source, target) return float(distance) / max(len(source), len(target)) def levenshtein(source: str, target: str) -> int: """Computes the Levenshtein (https://en.wikipedia.org/wiki/Levenshtein_distance) and restricted Damerau-Levenshtein (https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance) distances between two Unicode strings with given lengths using the Wagner-Fischer algorithm (https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm). These distances are defined recursively, since the distance between two strings is just the cost of adjusting the last one or two characters plus the distance between the prefixes that exclude these characters (e.g. the distance between "tester" and "tested" is 1 + the distance between "teste" and "teste"). The Wagner-Fischer algorithm retains this idea but eliminates redundant computations by storing the distances between various prefixes in a matrix that is filled in iteratively. """ # Create matrix of correct size (this is s_len + 1 * t_len + 1 so that the # empty prefixes "" can also be included). The leftmost column represents # transforming various source prefixes into an empty string, which can # always be done by deleting all characters in the respective prefix, and # the top row represents transforming the empty string into various target # prefixes, which can always be done by inserting every character in the # respective prefix. The ternary used to build the list should ensure that # this row and column are now filled correctly s_range = range(len(source) + 1) t_range = range(len(target) + 1) matrix = [[(i if j == 0 else j) for j in t_range] for i in s_range] # Iterate through rest of matrix, filling it in with Levenshtein # distances for the remaining prefix combinations for i in s_range[1:]: for j in t_range[1:]: # Applies the recursive logic outlined above using the values # stored in the matrix so far. The options for the last pair of # characters are deletion, insertion, and substitution, which # amount to dropping the source character, the target character, # or both and then calculating the distance for the resulting # prefix combo. If the characters at this point are the same, the # situation can be thought of as a free substitution del_dist = matrix[i - 1][j] + 1 ins_dist = matrix[i][j - 1] + 1 sub_trans_cost = 0 if source[i - 1] == target[j - 1] else 1 sub_dist = matrix[i - 1][j - 1] + sub_trans_cost # Choose option that produces smallest distance matrix[i][j] = min(del_dist, ins_dist, sub_dist) # At this point, the matrix is full, and the biggest prefixes are just the # strings themselves, so this is the desired distance return matrix[len(source)][len(target)] class DocoptLanguageError(Exception): """Error in construction of usage-message by developer.""" class DocoptExit(SystemExit): """Exit in case user invoked program with incorrect arguments.""" usage = "" def __init__( self, message: str = "", collected: list[Pattern] | None = None, left: list[Pattern] | None = None, ) -> None: self.collected = collected if collected is not None else [] self.left = left if left is not None else [] SystemExit.__init__(self, (message + "\n" + self.usage).strip()) class Pattern: def __init__( self, name: str | None, value: list[str] | str | int | None = None ) -> None: self._name, self.value = name, value @property def name(self) -> str | None: return self._name def __eq__(self, other) -> bool: return repr(self) == repr(other) def __hash__(self) -> int: return hash(repr(self)) def transform(pattern: BranchPattern) -> Either: """Expand pattern into an (almost) equivalent one, but with single Either. Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) Quirks: [-a] => (-a), (-a...) => (-a -a) """ result = [] groups = [[pattern]] while groups: children = groups.pop(0) parents = [Required, NotRequired, OptionsShortcut, Either, OneOrMore] if any(t in map(type, children) for t in parents): child = [c for c in children if type(c) in parents][0] children.remove(child) if type(child) is Either: for c in child.children: groups.append([c] + children) elif type(child) is OneOrMore: groups.append(child.children * 2 + children) else: groups.append(child.children + children) else: result.append(children) return Either(*[Required(*e) for e in result]) TSingleMatch = Tuple[Union[int, None], Union["LeafPattern", None]] class LeafPattern(Pattern): """Leaf/terminal node of a pattern tree.""" def __repr__(self) -> str: return "%s(%r, %r)" % (self.__class__.__name__, self.name, self.value) def single_match(self, left: list[LeafPattern]) -> TSingleMatch: raise NotImplementedError # pragma: no cover def flat(self, *types) -> list[LeafPattern]: return [self] if not types or type(self) in types else [] def match( self, left: list[LeafPattern], collected: list[Pattern] | None = None ) -> tuple[bool, list[LeafPattern], list[Pattern]]: collected = [] if collected is None else collected increment: Any | None = None pos, match = self.single_match(left) if match is None or pos is None: return False, left, collected left_ = left[:pos] + left[(pos + 1) :] same_name = [a for a in collected if a.name == self.name] if type(self.value) == int and len(same_name) > 0: if isinstance(same_name[0].value, int): same_name[0].value += 1 return True, left_, collected if type(self.value) == int and not same_name: match.value = 1 return True, left_, collected + [match] if same_name and type(self.value) == list: if type(match.value) == str: increment = [match.value] if same_name[0].value is not None and increment is not None: if isinstance(same_name[0].value, type(increment)): same_name[0].value += increment return True, left_, collected elif not same_name and type(self.value) == list: if isinstance(match.value, str): match.value = [match.value] return True, left_, collected + [match] return True, left_, collected + [match] class BranchPattern(Pattern): """Branch/inner node of a pattern tree.""" def __init__(self, *children) -> None: self.children = list(children) def match(self, left: list[Pattern], collected: list[Pattern] | None = None) -> Any: raise NotImplementedError # pragma: no cover def fix(self) -> "BranchPattern": self.fix_identities() self.fix_repeating_arguments() return self def fix_identities(self, uniq: Any | None = None) -> None: """Make pattern-tree tips point to same object if they are equal.""" flattened = self.flat() uniq = list(set(flattened)) if uniq is None else uniq for i, child in enumerate(self.children): if not hasattr(child, "children"): assert child in uniq self.children[i] = uniq[uniq.index(child)] else: child.fix_identities(uniq) return None def fix_repeating_arguments(self) -> BranchPattern: """Fix elements that should accumulate/increment values.""" either = [list(child.children) for child in transform(self).children] for case in either: for e in [child for child in case if case.count(child) > 1]: if type(e) is Argument or type(e) is Option and e.argcount: if e.value is None: e.value = [] elif type(e.value) is not list: e.value = cast(str, e.value) e.value = e.value.split() if type(e) is Command or type(e) is Option and e.argcount == 0: e.value = 0 return self def __repr__(self) -> str: return "%s(%s)" % ( self.__class__.__name__, ", ".join(repr(a) for a in self.children), ) def flat(self, *types) -> Any: if type(self) in types: return [self] return sum([child.flat(*types) for child in self.children], []) class Argument(LeafPattern): def single_match(self, left: list[LeafPattern]) -> TSingleMatch: for n, pattern in enumerate(left): if type(pattern) is Argument: return n, Argument(self.name, pattern.value) return None, None class Command(Argument): def __init__(self, name: str | None, value: bool = False) -> None: self._name, self.value = name, value def single_match(self, left: list[LeafPattern]) -> TSingleMatch: for n, pattern in enumerate(left): if type(pattern) is Argument: if pattern.value == self.name: return n, Command(self.name, True) else: break return None, None class Option(LeafPattern): def __init__( self, short: str | None = None, longer: str | None = None, argcount: int = 0, value: list[str] | str | int | None = False, ) -> None: assert argcount in (0, 1) self.short, self.longer, self.argcount = short, longer, argcount self.value = None if value is False and argcount else value @classmethod def parse(class_, option_description: str) -> Option: short, longer, argcount, value = None, None, 0, False options, description = re.split( r"(?: )|$", option_description.strip(), flags=re.M, maxsplit=1 ) options = options.replace(",", " ").replace("=", " ") for s in options.split(): if s.startswith("--"): longer = s elif s.startswith("-"): short = s else: argcount = 1 if argcount: matched = re.findall(r"\[default: (.*)\]", description, flags=re.I) value = matched[0] if matched else None return class_(short, longer, argcount, value) def single_match(self, left: list[LeafPattern]) -> TSingleMatch: for n, pattern in enumerate(left): if self.name == pattern.name: return n, pattern return None, None @property def name(self) -> str | None: return self.longer or self.short def __repr__(self) -> str: return "Option(%r, %r, %r, %r)" % ( self.short, self.longer, self.argcount, self.value, ) class Required(BranchPattern): def match(self, left: list[Pattern], collected: list[Pattern] | None = None) -> Any: collected = [] if collected is None else collected original_collected = collected original_left = left for pattern in self.children: matched, left, collected = pattern.match(left, collected) if not matched: return False, original_left, original_collected return True, left, collected class NotRequired(BranchPattern): def match(self, left: list[Pattern], collected: list[Pattern] | None = None) -> Any: collected = [] if collected is None else collected for pattern in self.children: _, left, collected = pattern.match(left, collected) return True, left, collected class OptionsShortcut(NotRequired): """Marker/placeholder for [options] shortcut.""" class OneOrMore(BranchPattern): def match(self, left: list[Pattern], collected: list[Pattern] | None = None) -> Any: assert len(self.children) == 1 collected = [] if collected is None else collected original_collected = collected original_left = left last_left = None matched = True times = 0 while matched: matched, left, collected = self.children[0].match(left, collected) times += 1 if matched else 0 if last_left == left: break last_left = left if times >= 1: return True, left, collected return False, original_left, original_collected class Either(BranchPattern): def match(self, left: list[Pattern], collected: list[Pattern] | None = None) -> Any: collected = [] if collected is None else collected outcomes = [] for pattern in self.children: matched, _, _ = outcome = pattern.match(left, collected) if matched: outcomes.append(outcome) if outcomes: return min(outcomes, key=lambda outcome: len(outcome[1])) return False, left, collected class Tokens(list): def __init__( self, source: list[str] | str, error: Type[DocoptExit] | Type[DocoptLanguageError] = DocoptExit, ) -> None: if isinstance(source, list): self += source else: self += source.split() self.error = error @staticmethod def from_pattern(source: str) -> Tokens: source = re.sub(r"([\[\]\(\)\|]|\.\.\.)", r" \1 ", source) fragments = [s for s in re.split(r"\s+|(\S*<.*?>)", source) if s] return Tokens(fragments, error=DocoptLanguageError) def move(self) -> str | None: return self.pop(0) if len(self) else None def current(self) -> str | None: return self[0] if len(self) else None def parse_longer( tokens: Tokens, options: list[Option], argv: bool = False, more_magic: bool = False ) -> list[Pattern]: """longer ::= '--' chars [ ( ' ' | '=' ) chars ] ;""" current_token = tokens.move() if current_token is None or not current_token.startswith("--"): raise ValueError( f"parse_longer got what appears to be an invalid token: {current_token}" ) longer, maybe_eq, maybe_value = current_token.partition("=") if maybe_eq == maybe_value == "": value = None else: value = maybe_value similar = [o for o in options if o.longer and longer == o.longer] start_collision = ( len( [ o for o in options if o.longer and longer in o.longer and o.longer.startswith(longer) ] ) > 1 ) if argv and not len(similar) and not start_collision: similar = [ o for o in options if o.longer and longer in o.longer and o.longer.startswith(longer) ] # try advanced matching if more_magic and not similar: corrected = [ (longer, o) for o in options if o.longer and levenshtein_norm(longer, o.longer) < 0.25 ] if corrected: print(f"NB: Corrected {corrected[0][0]} to {corrected[0][1].longer}") similar = [correct for (original, correct) in corrected] if len(similar) > 1: raise DocoptLanguageError(f"{longer} is not a unique prefix: {similar}?") elif len(similar) < 1: argcount = 1 if maybe_eq == "=" else 0 o = Option(None, longer, argcount) options.append(o) if tokens.error is DocoptExit: o = Option(None, longer, argcount, value if argcount else True) else: o = Option( similar[0].short, similar[0].longer, similar[0].argcount, similar[0].value ) if o.argcount == 0: if value is not None: raise tokens.error("%s must not have an argument" % o.longer) else: if value is None: if tokens.current() in [None, "--"]: raise tokens.error("%s requires argument" % o.longer) value = tokens.move() if tokens.error is DocoptExit: o.value = value if value is not None else True return [o] def parse_shorts( tokens: Tokens, options: list[Option], more_magic: bool = False ) -> list[Pattern]: """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;""" token = tokens.move() if token is None or not token.startswith("-") or token.startswith("--"): raise ValueError( f"parse_shorts got what appears to be an invalid token: {token}" ) left = token.lstrip("-") parsed: list[Pattern] = [] while left != "": short, left = "-" + left[0], left[1:] transformations: dict[str | None, Callable[[str], str]] = {None: lambda x: x} if more_magic: transformations["lowercase"] = lambda x: x.lower() transformations["uppercase"] = lambda x: x.upper() # try identity, lowercase, uppercase, iff such resolves uniquely # (ie if upper and lowercase are not both defined) similar: list[Option] = [] de_abbreviated = False for transform_name, transform in transformations.items(): transformed = list(set([transform(o.short) for o in options if o.short])) no_collisions = len( [ o for o in options if o.short and transformed.count(transform(o.short)) == 1 ] ) # == len(transformed) if no_collisions: similar = [ o for o in options if o.short and transform(o.short) == transform(short) ] if similar: if transform_name: print( f"NB: Corrected {short} to {similar[0].short} " f"via {transform_name}" ) break # if transformations do not resolve, try abbreviations of 'longer' forms # iff such resolves uniquely (ie if no two longer forms begin with the # same letter) if not similar and more_magic: abbreviated = [ transform(o.longer[1:3]) for o in options if o.longer and not o.short ] + [transform(o.short) for o in options if o.short and not o.longer] nonredundantly_abbreviated_options = [ o for o in options if o.longer and abbreviated.count(short) == 1 ] no_collisions = len(nonredundantly_abbreviated_options) == len( abbreviated ) if no_collisions: for o in options: if ( not o.short and o.longer and transform(short) == transform(o.longer[1:3]) ): similar = [o] print( f"NB: Corrected {short} to {similar[0].longer} " f"via abbreviation (case change: {transform_name})" ) break if len(similar): de_abbreviated = True break if len(similar) > 1: raise DocoptLanguageError( f"{short} is specified ambiguously {len(similar)} times" ) elif len(similar) < 1: o = Option(short, None, 0) options.append(o) if tokens.error is DocoptExit: o = Option(short, None, 0, True) else: if de_abbreviated: option_short_value = None else: option_short_value = transform(short) o = Option( option_short_value, similar[0].longer, similar[0].argcount, similar[0].value, ) value = None current_token = tokens.current() if o.argcount != 0: if left == "": if current_token is None or current_token == "--": raise tokens.error("%s requires argument" % short) else: value = tokens.move() else: value = left left = "" if tokens.error is DocoptExit: o.value = value if value is not None else True parsed.append(o) return parsed def parse_pattern(source: str, options: list[Option]) -> Required: tokens = Tokens.from_pattern(source) result = parse_expr(tokens, options) if tokens.current() is not None: raise tokens.error("unexpected ending: %r" % " ".join(tokens)) return Required(*result) def parse_expr(tokens: Tokens, options: list[Option]) -> list[Pattern]: """expr ::= seq ( '|' seq )* ;""" result: list[Pattern] = [] seq_0: list[Pattern] = parse_seq(tokens, options) if tokens.current() != "|": return seq_0 if len(seq_0) > 1: result.append(Required(*seq_0)) else: result += seq_0 while tokens.current() == "|": tokens.move() seq_1 = parse_seq(tokens, options) if len(seq_1) > 1: result += [Required(*seq_1)] else: result += seq_1 return [Either(*result)] def parse_seq(tokens: Tokens, options: list[Option]) -> list[Pattern]: """seq ::= ( atom [ '...' ] )* ;""" result: list[Pattern] = [] while tokens.current() not in [None, "]", ")", "|"]: atom = parse_atom(tokens, options) if tokens.current() == "...": atom = [OneOrMore(*atom)] tokens.move() result += atom return result def parse_atom(tokens: Tokens, options: list[Option]) -> list[Pattern]: """atom ::= '(' expr ')' | '[' expr ']' | 'options' | longer | shorts | argument | command ; """ token = tokens.current() if not token: return [Command(tokens.move())] # pragma: no cover elif token in "([": tokens.move() matching = {"(": ")", "[": "]"}[token] pattern = {"(": Required, "[": NotRequired}[token] matched_pattern = pattern(*parse_expr(tokens, options)) if tokens.move() != matching: raise tokens.error("unmatched '%s'" % token) return [matched_pattern] elif token == "options": tokens.move() return [OptionsShortcut()] elif token.startswith("--") and token != "--": return parse_longer(tokens, options) elif token.startswith("-") and token not in ("-", "--"): return parse_shorts(tokens, options) elif token.startswith("<") and token.endswith(">") or token.isupper(): return [Argument(tokens.move())] else: return [Command(tokens.move())] def parse_argv( tokens: Tokens, options: list[Option], options_first: bool = False, more_magic: bool = False, ) -> list[Pattern]: """Parse command-line argument vector. If options_first: argv ::= [ longer | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; else: argv ::= [ longer | shorts | argument ]* [ '--' [ argument ]* ] ; """ def isanumber(x): try: float(x) return True except ValueError: return False parsed: list[Pattern] = [] current_token = tokens.current() while current_token is not None: if current_token == "--": return parsed + [Argument(None, v) for v in tokens] elif current_token.startswith("--"): parsed += parse_longer(tokens, options, argv=True, more_magic=more_magic) elif ( current_token.startswith("-") and current_token != "-" and not isanumber(current_token) ): parsed += parse_shorts(tokens, options, more_magic=more_magic) elif options_first: return parsed + [Argument(None, v) for v in tokens] else: parsed.append(Argument(None, tokens.move())) current_token = tokens.current() return parsed class DocSections(NamedTuple): before_usage: str usage_header: str usage_body: str after_usage: str def parse_docstring_sections(docstring: str) -> DocSections: """Partition the docstring into the main sections. The docstring is returned, split into a tuple of 4 pieces: text before the usage section, the usage section header, the usage section body and text following the usage section. """ usage_pattern = r""" # Any number of lines (that don't include usage:) precede the usage section \A(?P(?:(?!.*\busage:).*\n)*) # The `usage:` section header. ^(?P.*\busage:) (?P # The first line of the body may follow the header without a line break: (?:.*(?:\n|\Z)) # Any number of additional indented lines (?:[ \t].*(?:\n|\Z))* ) # Everything else (?P(?:.|\n)*)\Z """ match = re.match(usage_pattern, docstring, flags=re.M | re.I | re.VERBOSE) if not match: raise DocoptLanguageError( 'Failed to parse doc: "usage:" section (case-insensitive) not found. ' "Check http://docopt.org/ for examples of how your doc should look." ) before, header, body, after = match.groups() return DocSections(before, header, body, after) def parse_options(docstring: str) -> list[Option]: """Parse the option descriptions from the help text. `docstring` is the sub-section of the overall docstring that option descriptions should be parsed from. It must not contain the "usage:" section, as wrapped lines in the usage pattern can be misinterpreted as option descriptions. Option descriptions appear below the usage patterns, They define synonymous long and short options, options that have arguments, and the default values of options' arguments. They look like this: ``` -v, --verbose Be more verbose -n COUNT, --number COUNT The number of times to do the thing [default: 42] ``` """ option_start = r""" # Option descriptions begin on a new line ^ # They may occur on the same line as an options: section heading (?:.*options:)? # They can be indented with whitespace [ \t]* # The description itself starts with the short or long flag (-x or --xxx) (-\S) """ parts = re.split(option_start, docstring, flags=re.M | re.I | re.VERBOSE)[1:] return [ Option.parse(start + rest) for (start, rest) in zip(parts[0::2], parts[1::2]) ] def lint_docstring(sections: DocSections): """Report apparent mistakes in the docstring format.""" if re.search("options:", sections.usage_body, flags=re.I): raise DocoptLanguageError( 'Failed to parse docstring: "options:" (case-insensitive) was ' 'found in "usage:" section. Use a blank line after the usage, or ' "start the next section without leading whitespace." ) if re.search("usage:", sections.usage_body + sections.after_usage, flags=re.I): raise DocoptLanguageError( 'Failed to parse docstring: More than one "usage:" ' "(case-insensitive) section found." ) if sections.usage_body.strip() == "": raise DocoptLanguageError( 'Failed to parse docstring: "usage:" section is empty.' "Check http://docopt.org/ for examples of how your doc should look." ) def formal_usage(usage: str) -> str: program_name, *tokens = usage.split() return "( " + " ".join(") | (" if s == program_name else s for s in tokens) + " )" def extras( default_help: bool, version: None, options: list[Pattern], docstring: str ) -> None: if default_help and any( (o.name in ("-h", "--help")) and o.value for o in options if isinstance(o, Option) ): print(docstring.strip("\n")) sys.exit() if version and any( o.name == "--version" and o.value for o in options if isinstance(o, Option) ): print(version) sys.exit() class ParsedOptions(dict): def __repr__(self): return "{%s}" % ",\n ".join("%r: %r" % i for i in sorted(self.items())) def __getattr__(self, name: str) -> str | bool | None: return self.get(name) or { name: self.get(k) for k in self.keys() if name in [k.lstrip("-").replace("-", "_"), k.lstrip("<").rstrip(">")] }.get(name) def docopt( docstring: str, argv: list[str] | str | None = None, default_help: bool = True, version: Any = None, options_first: bool = False, ) -> ParsedOptions: """Parse `argv` based on command-line interface described in `docstring`. `docopt` creates your command-line interface based on its description that you pass as `docstring`. Such description can contain --options, , commands, which could be [optional], (required), (mutually | exclusive) or repeated... Parameters ---------- docstring : str Description of your command-line interface. argv : list of str or str, optional Argument vector to be parsed. sys.argv[1:] is used if not provided. If str is passed, the string is split on whitespace. default_help : bool (default: True) Set to False to disable automatic help on -h or --help options. version : any object If passed, the object will be printed if --version is in `argv`. options_first : bool (default: False) Set to True to require options precede positional arguments, i.e. to forbid options and positional arguments intermix. Returns ------- arguments: dict-like A dictionary, where keys are names of command-line elements such as e.g. "--verbose" and "", and values are the parsed values of those elements. Also supports dot access. Example ------- >>> from docopt import docopt >>> doc = ''' ... Usage: ... my_program tcp [--timeout=] ... my_program serial [--baud=] [--timeout=] ... my_program (-h | --help | --version) ... ... Options: ... -h, --help Show this screen and exit. ... --baud= Baudrate [default: 9600] ... ''' >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] >>> docopt(doc, argv) {'--baud': '9600', '--help': False, '--timeout': '30', '--version': False, '': '127.0.0.1', '': '80', 'serial': False, 'tcp': True} """ argv = sys.argv[1:] if argv is None else argv sections = parse_docstring_sections(docstring) lint_docstring(sections) DocoptExit.usage = sections.usage_header + sections.usage_body options = [ *parse_options(sections.before_usage), *parse_options(sections.after_usage), ] pattern = parse_pattern(formal_usage(sections.usage_body), options) pattern_options = set(pattern.flat(Option)) for options_shortcut in pattern.flat(OptionsShortcut): options_shortcut.children = [ opt for opt in options if opt not in pattern_options ] parsed_arg_vector = parse_argv(Tokens(argv), list(options), options_first) extras(default_help, version, parsed_arg_vector, docstring) matched, left, collected = pattern.fix().match(parsed_arg_vector) if matched and left == []: return ParsedOptions((a.name, a.value) for a in (pattern.flat() + collected)) if left: raise DocoptExit(f"Warning: found unmatched (duplicate?) arguments {left}") raise DocoptExit(collected=collected, left=left) docopt-ng-0.9.0/docopt/_version.py000066400000000000000000000000261443545732600171130ustar00rootroot00000000000000__version__ = "0.9.0" docopt-ng-0.9.0/docopt/py.typed000066400000000000000000000000001443545732600164040ustar00rootroot00000000000000docopt-ng-0.9.0/examples/000077500000000000000000000000001443545732600152455ustar00rootroot00000000000000docopt-ng-0.9.0/examples/arguments_example.py000066400000000000000000000011211443545732600213320ustar00rootroot00000000000000"""Usage: arguments_example.py [-vqrh] [FILE] ... arguments_example.py (--left | --right) CORRECTION FILE Process FILE and optionally apply correction to either left-hand side or right-hand side. Arguments: FILE optional input file CORRECTION correction angle, needs FILE, --left or --right to be present Options: -h --help -v verbose mode -q quiet mode -r make report --left use left-hand side --right use right-hand side """ from docopt import docopt if __name__ == "__main__": arguments = docopt(__doc__) print(arguments) docopt-ng-0.9.0/examples/calculator_example.py000066400000000000000000000007711443545732600214700ustar00rootroot00000000000000"""Not a serious example. Usage: calculator_example.py ( ( + | - | * | / ) )... calculator_example.py [( , )]... calculator_example.py (-h | --help) Examples: calculator_example.py 1 + 2 + 3 + 4 + 5 calculator_example.py 1 + 2 '*' 3 / 4 - 5 # note quotes around '*' calculator_example.py sum 10 , 20 , 30 , 40 Options: -h, --help """ from docopt import docopt if __name__ == "__main__": arguments = docopt(__doc__) print(arguments) docopt-ng-0.9.0/examples/config_file_example.py000066400000000000000000000042631443545732600216030ustar00rootroot00000000000000"""Usage: quick_example.py tcp [] [--force] [--timeout=] quick_example.py serial [--baud=] [--timeout=] quick_example.py -h | --help | --version """ from docopt import docopt def load_json_config(): import json # Pretend that we load the following JSON file: source = """ {"--force": true, "--timeout": "10", "--baud": "9600"} """ return json.loads(source) def load_ini_config(): try: # Python 2 from ConfigParser import ConfigParser from StringIO import StringIO except ImportError: # Python 3 from configparser import ConfigParser from io import StringIO # By using `allow_no_value=True` we are allowed to # write `--force` instead of `--force=true` below. config = ConfigParser(allow_no_value=True) # Pretend that we load the following INI file: source = """ [default-arguments] --force --baud=19200 =localhost """ # ConfigParser requires a file-like object and # no leading whitespace. config_file = StringIO("\n".join(source.split())) config.readfp(config_file) # ConfigParsers sets keys which have no value # (like `--force` above) to `None`. Thus we # need to substitute all `None` with `True`. return dict( (key, True if value is None else value) for key, value in config.items("default-arguments") ) def merge(dict_1, dict_2): """Merge two dictionaries. Values that evaluate to true take priority over falsy values. `dict_1` takes priority over `dict_2`. """ return dict( (str(key), dict_1.get(key) or dict_2.get(key)) for key in set(dict_2) | set(dict_1) ) if __name__ == "__main__": json_config = load_json_config() ini_config = load_ini_config() arguments = docopt(__doc__, version="0.1.1rc") # Arguments take priority over INI, INI takes priority over JSON: result = merge(arguments, merge(ini_config, json_config)) from pprint import pprint print("\nJSON config:") pprint(json_config) print("\nINI config:") pprint(ini_config) print("\nResult:") pprint(result) docopt-ng-0.9.0/examples/counted_example.py000066400000000000000000000006241443545732600207750ustar00rootroot00000000000000"""Usage: counted_example.py --help counted_example.py -v... counted_example.py go [go] counted_example.py (--path=)... counted_example.py Try: counted_example.py -vvvvvvvvvv counted_example.py go go counted_example.py --path ./here --path ./there counted_example.py this.txt that.txt """ from docopt import docopt print(docopt(__doc__)) docopt-ng-0.9.0/examples/git/000077500000000000000000000000001443545732600160305ustar00rootroot00000000000000docopt-ng-0.9.0/examples/git/git.py000077500000000000000000000034501443545732600171720ustar00rootroot00000000000000#! /usr/bin/env python """ usage: git [--version] [--exec-path=] [--html-path] [-p|--paginate|--no-pager] [--no-replace-objects] [--bare] [--git-dir=] [--work-tree=] [-c =] [--help] [...] options: -c -h, --help -p, --paginate The most commonly used git commands are: add Add file contents to the index branch List, create, or delete branches checkout Checkout a branch or paths to the working tree clone Clone a repository into a new directory commit Record changes to the repository push Update remote refs along with associated objects remote Manage set of tracked repositories See 'git help ' for more information on a specific command. """ from subprocess import call from docopt import docopt if __name__ == "__main__": args = docopt(__doc__, version="git version 1.7.4.4", options_first=True) print("global arguments:") print(args) print("command arguments:") argv = [args[""]] + args[""] if args[""] == "add": # In case subcommand is implemented as python module: import git_add print(docopt(git_add.__doc__, argv=argv)) elif args[""] == "branch": # In case subcommand is a script in some other programming language: exit(call(["python", "git_branch.py"] + argv)) elif args[""] in "checkout clone commit push remote".split(): # For the rest we'll just keep DRY: exit(call(["python", "git_%s.py" % args[""]] + argv)) elif args[""] in ["help", None]: exit(call(["python", "git.py", "--help"])) else: exit("%r is not a git.py command. See 'git help'." % args[""]) docopt-ng-0.9.0/examples/git/git_add.py000066400000000000000000000015221443545732600177750ustar00rootroot00000000000000"""usage: git add [options] [--] [...] -h, --help -n, --dry-run dry run -v, --verbose be verbose -i, --interactive interactive picking -p, --patch select hunks interactively -e, --edit edit current diff and apply -f, --force allow adding otherwise ignored files -u, --update update tracked files -N, --intent-to-add record only the fact that the path will be added later -A, --all add all, noticing removal of tracked files --refresh don't add, only refresh the index --ignore-errors just skip files which cannot be added because of errors --ignore-missing check if - even missing - files are ignored in dry run """ from docopt import docopt if __name__ == "__main__": print(docopt(__doc__)) docopt-ng-0.9.0/examples/git/git_branch.py000066400000000000000000000025531443545732600205070ustar00rootroot00000000000000""" usage: git branch [options] [-r | -a] [--merged= | --no-merged=] git branch [options] [-l] [-f] [] git branch [options] [-r] (-d | -D) git branch [options] (-m | -M) [] Generic options -h, --help -v, --verbose show hash and subject, give twice for upstream branch -t, --track set up tracking mode (see git-pull(1)) --set-upstream change upstream info --color= use colored output -r act on remote-tracking branches --contains= print only branches that contain the commit --abbrev= use digits to display SHA-1s Specific git-branch actions: -a list both remote-tracking and local branches -d delete fully merged branch -D delete branch (even if not merged) -m move/rename a branch and its reflog -M move/rename a branch, even if target exists -l create the branch's reflog -f, --force force creation (when already exists) --no-merged= print only not merged branches --merged= print only merged branches """ from docopt import docopt if __name__ == "__main__": print(docopt(__doc__)) docopt-ng-0.9.0/examples/git/git_checkout.py000066400000000000000000000016421443545732600210550ustar00rootroot00000000000000"""usage: git checkout [options] git checkout [options] -- ... -q, --quiet suppress progress reporting -b create and checkout a new branch -B create/reset and checkout a branch -l create reflog for new branch -t, --track set upstream info for new branch --orphan new unparented branch -2, --ours checkout our version for unmerged files -3, --theirs checkout their version for unmerged files -f, --force force checkout (throw away local modifications) -m, --merge perform a 3-way merge with the new branch --conflict