Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix-311 #51

Merged
merged 1 commit into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 8 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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:]`).
Expand Down Expand Up @@ -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
Expand Down
74 changes: 8 additions & 66 deletions docopt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"""
from __future__ import annotations

import inspect
import re
import sys
from typing import Any
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
-------
Expand Down Expand Up @@ -907,48 +901,8 @@ def docopt(
'<port>': '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
Expand All @@ -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
43 changes: 0 additions & 43 deletions examples/more_magic_example.py

This file was deleted.