From eaf47e7c47afc90b716429e133c517f2853624a5 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 30 May 2023 12:23:34 -0800 Subject: [PATCH] BREAKING: remove more_magic This feature has many problems. See CHANGELOG.md for a rationale --- CHANGELOG.md | 26 ++++++ README.md | 26 ++---- docopt/__init__.py | 74 ++------------- examples/more_magic_example.py | 43 --------- tests/test_docopt_ng.py | 166 --------------------------------- 5 files changed, 42 insertions(+), 293 deletions(-) delete mode 100644 examples/more_magic_example.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d43f6..d17f84e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,32 @@ ## UNRELEASED ### 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 diff --git a/README.md b/README.md index 6cd24f2..d7c84a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# **docopt-ng** creates *magic* command-line interfaces +# **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) @@ -9,7 +9,7 @@ **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 *magically*: +**docopt-ng** helps you create beautiful command-line interfaces: ```python """Naval Fate. @@ -81,22 +81,20 @@ Use [pip](http://pip-installer.org): ```python def docopt( - docstring: str | None = None, + docstring: str, argv: list[str] | str | None = None, default_help: bool = True, version: Any = None, options_first: bool = False, - more_magic: bool = False, ) -> ParsedOptions: ``` -`docopt` takes 6 optional arguments: +`docopt` takes a docstring, and 4 optional arguments: -- `docstring` could be a module docstring (`__doc__`) or some other string - that contains a **help message** that will be parsed to create the - option parser. The simple rules of how to write such a help message - are given in next sections. If it is None (not provided), the calling scope - will be interrogated for a docstring. +- `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:]`). @@ -129,14 +127,6 @@ def docopt( with POSIX, or if you want to dispatch your arguments to other programs. -- `more_magic`, by default `False`. If set to `True` more advanced - efforts will be made to correct `--long_form` arguments, ie: - `--hlep` will be corrected to `--help`. Additionally, if not - already defined, the variable `arguments` will be created and populated - in the calling scope. `more_magic` is also set True if `docopt()` is - is aliased to a name containing `magic` ie) by built-in`from docopt import magic` or - user-defined `from docopt import docopt as magic_docopt_wrapper` for convenience. - 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 diff --git a/docopt/__init__.py b/docopt/__init__.py index 1c36ea3..648eb8f 100644 --- a/docopt/__init__.py +++ b/docopt/__init__.py @@ -23,7 +23,6 @@ """ from __future__ import annotations -import inspect import re import sys from typing import Any @@ -36,7 +35,7 @@ from ._version import __version__ as __version__ -__all__ = ["docopt", "magic_docopt", "magic", "DocoptExit"] +__all__ = ["docopt", "DocoptExit"] def levenshtein_norm(source: str, target: str) -> float: @@ -842,14 +841,13 @@ def __getattr__(self, name: str) -> str | bool | None: def docopt( - docstring: str | None = None, + docstring: str, argv: list[str] | str | None = None, default_help: bool = True, version: Any = None, options_first: bool = False, - more_magic: bool = False, ) -> ParsedOptions: - """Parse `argv` based on command-line interface described in `doc`. + """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 @@ -858,11 +856,11 @@ def docopt( Parameters ---------- - docstring : str (default: first __doc__ in parent scope) + docstring : str Description of your command-line interface. - argv : list of str, optional + argv : list of str or str, optional Argument vector to be parsed. sys.argv[1:] is used if not - provided. + 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. @@ -872,10 +870,6 @@ def docopt( options_first : bool (default: False) Set to True to require options precede positional arguments, i.e. to forbid options and positional arguments intermix. - more_magic : bool (default: False) - Try to be extra-helpful; pull results into globals() of caller as 'arguments', - offer advanced pattern-matching and spellcheck. - Also activates if `docopt` aliased to a name containing 'magic'. Returns ------- @@ -907,48 +901,8 @@ def docopt( '': '80', 'serial': False, 'tcp': True} - """ argv = sys.argv[1:] if argv is None else argv - maybe_frame = inspect.currentframe() - if maybe_frame: - parent_frame = doc_parent_frame = magic_parent_frame = maybe_frame.f_back - if not more_magic: # make sure 'magic' isn't in the calling name - while not more_magic and magic_parent_frame: - imported_as = { - v: k - for k, v in magic_parent_frame.f_globals.items() - if hasattr(v, "__name__") and v.__name__ == docopt.__name__ - }.get(docopt) - if imported_as and "magic" in imported_as: - more_magic = True - else: - magic_parent_frame = magic_parent_frame.f_back - if not docstring: # go look for one, if none exists, raise Exception - while not docstring and doc_parent_frame: - docstring = doc_parent_frame.f_locals.get("__doc__") - if not docstring: - doc_parent_frame = doc_parent_frame.f_back - if not docstring: - raise DocoptLanguageError( - "Either __doc__ must be defined in the scope of a parent " - "or passed as the first argument." - ) - output_value_assigned = False - if more_magic and parent_frame: - import dis - - instrs = dis.get_instructions(parent_frame.f_code) - for instr in instrs: - if instr.offset == parent_frame.f_lasti: - break - assert instr.opname.startswith("CALL_") - MAYBE_STORE = next(instrs) - if MAYBE_STORE and ( - MAYBE_STORE.opname.startswith("STORE") - or MAYBE_STORE.opname.startswith("RETURN") - ): - output_value_assigned = True sections = parse_docstring_sections(docstring) lint_docstring(sections) DocoptExit.usage = sections.usage_header + sections.usage_body @@ -962,23 +916,11 @@ def docopt( 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, more_magic - ) + 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 == []: - output_obj = ParsedOptions( - (a.name, a.value) for a in (pattern.flat() + collected) - ) - target_parent_frame = parent_frame or magic_parent_frame or doc_parent_frame - if more_magic and target_parent_frame and not output_value_assigned: - if not target_parent_frame.f_globals.get("arguments"): - target_parent_frame.f_globals["arguments"] = output_obj - return output_obj + 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) - - -magic = magic_docopt = docopt diff --git a/examples/more_magic_example.py b/examples/more_magic_example.py deleted file mode 100644 index 1f6e623..0000000 --- a/examples/more_magic_example.py +++ /dev/null @@ -1,43 +0,0 @@ -__doc__ = f""" -Usage: {__file__} [options] [FILE] ... - {__file__} (--left | --right) STEP1 STEP2 - -Process FILE and optionally apply correction to either left-hand side or -right-hand side. - -Arguments: - FILE optional input file - STEP1 STEP2 STEP1, STEP2 --left or --right to be present - -Options: - --help - --verbose verbose mode - --quiet quiet mode - --report make report - -""" - -from docopt import magic - -magic() - -""" -prints: -{'--help': False, - '--left': False, - '--quiet': False, - '--report': False, - '--right': False, - '--verbose': False, - 'FILE': [], - 'STEP1': None, - 'STEP2': None} -""" - -print(arguments) # noqa: F821 - -""" -prints: -False -""" -print(arguments.left) # noqa: F821 diff --git a/tests/test_docopt_ng.py b/tests/test_docopt_ng.py index d68fb92..90eb73e 100644 --- a/tests/test_docopt_ng.py +++ b/tests/test_docopt_ng.py @@ -1,15 +1,10 @@ import pytest -from pytest import raises import docopt from docopt import Argument from docopt import DocoptExit -from docopt import DocoptLanguageError from docopt import Option from docopt import Tokens -from docopt import docopt as user_provided_alias_containing_magic -from docopt import magic -from docopt import magic_docopt from docopt import parse_argv @@ -44,151 +39,6 @@ def TS(s): ] -def test_docopt_ng_as_magic_docopt_more_magic_global_arguments_and_dot_access(): - doc = """Usage: prog [-vqr] [FILE] - prog INPUT OUTPUT - prog --help - - Options: - -v print status messages - -q report only file names - -r show all occurrences of the same error - --help - - """ - global arguments - magic_docopt(doc, "-v file.py") - assert arguments == { - "-v": True, - "-q": False, - "-r": False, - "--help": False, - "FILE": "file.py", - "INPUT": None, - "OUTPUT": None, - } - assert arguments.v - assert arguments.FILE == "file.py" - arguments = None - magic(doc, "-v file.py") - assert arguments == { - "-v": True, - "-q": False, - "-r": False, - "--help": False, - "FILE": "file.py", - "INPUT": None, - "OUTPUT": None, - } - assert arguments.v - assert arguments.FILE == "file.py" - arguments = None - user_provided_alias_containing_magic(doc, "-v file.py") - assert arguments == { - "-v": True, - "-q": False, - "-r": False, - "--help": False, - "FILE": "file.py", - "INPUT": None, - "OUTPUT": None, - } - - -def test_docopt_ng_more_magic_no_make_global_arguments_if_assigned(): - doc = """Usage: prog [-vqr] [FILE] - prog INPUT OUTPUT - prog --help - - Options: - -v print status messages - -q report only file names - -r show all occurrences of the same error - --help - - """ - global arguments - global arguments - arguments = None - opts = magic_docopt(doc, argv="-v file.py") - assert arguments is None - assert opts == { - "-v": True, - "-q": False, - "-r": False, - "--help": False, - "FILE": "file.py", - "INPUT": None, - "OUTPUT": None, - } - assert opts.v - assert opts.FILE == "file.py" - - -def test_docopt_ng_more_magic_global_arguments_and_dot_access(): - doc = """Usage: prog [-vqr] [FILE] - prog INPUT OUTPUT - prog --help - - Options: - -v print status messages - -q report only file names - -r show all occurrences of the same error - --help - - """ - global arguments - docopt.docopt(doc, "-v file.py", more_magic=True) - assert arguments == { - "-v": True, - "-q": False, - "-r": False, - "--help": False, - "FILE": "file.py", - "INPUT": None, - "OUTPUT": None, - } - assert arguments.v - assert arguments.FILE == "file.py" - arguments = None - docopt.docopt(doc.replace("FILE", ""), "-v", more_magic=True) - assert arguments == { - "-v": True, - "-q": False, - "-r": False, - "--help": False, - "": None, - "INPUT": None, - "OUTPUT": None, - } - assert arguments.FILE is None - - with raises( - DocoptExit, - match=r"Warning: found unmatched \(duplicate\?\) arguments.*output\.py", - ): - docopt.docopt(doc, "-v input.py output.py") - - with raises( - DocoptExit, - match=r"Warning: found unmatched \(duplicate\?\) arguments.*--fake", - ): - docopt.docopt(doc, "--fake") - arguments = None - - -def test_docopt_ng__doc__if_no_doc(): - import sys - - __doc__, sys.argv = "usage: prog --long=", [None, "--long="] - assert docopt.docopt() == {"--long": ""} - __doc__, sys.argv = "usage:\n\tprog -l \noptions:\n\t-l \n", [None, "-l", ""] - assert docopt.docopt() == {"-l": ""} - __doc__, sys.argv = None, [None, "-l", ""] # noqa: F841 - with raises(DocoptLanguageError): - docopt.docopt() - - def test_docopt_ng_negative_float(): args = docopt.docopt( "usage: prog --negative_pi=NEGPI NEGTAU", "--negative_pi -3.14 -6.28" @@ -203,22 +53,6 @@ def test_docopt_ng_doubledash_version(capsys: pytest.CaptureFixture): assert pytest_wrapped_e.type == SystemExit -def test_docopt_ng__doc__if_no_doc_indirection(): - import sys - - __doc__, sys.argv = "usage: prog --long=", [None, "--long="] # noqa: F841 - - def test_indirect(): - return docopt.docopt() - - assert test_indirect() == {"--long": ""} - - def test_even_more_indirect(): - return test_indirect() - - assert test_even_more_indirect() == {"--long": ""} - - def test_docopt_ng_dot_access_with_dash(): doc = """Usage: prog [-vqrd] [FILE] prog INPUT OUTPUT