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

Assert error message content in tests #37

Merged
merged 12 commits into from
Sep 9, 2022
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Run tests
run: |
pytest
pytest --cov-report term-missing --cov-report xml --cov=docopt --mypy

- name: Upload coverage
uses: codecov/codecov-action@v2
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.*
!.github
!.gitignore
!.pre-commit-config.yaml

*.py[co]

# Vim
Expand Down
14 changes: 6 additions & 8 deletions docopt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,9 @@ def parse_longer(
"""longer ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
current_token = tokens.move()
if current_token is None or not current_token.startswith("--"):
raise tokens.error(
raise ValueError(
f"parse_longer got what appears to be an invalid token: {current_token}"
) # pragma: no cover
)
longer, maybe_eq, maybe_value = current_token.partition("=")
if maybe_eq == maybe_value == "":
value = None
Expand Down Expand Up @@ -463,9 +463,7 @@ def parse_longer(
print(f"NB: Corrected {corrected[0][0]} to {corrected[0][1].longer}")
similar = [correct for (original, correct) in corrected]
if len(similar) > 1:
raise tokens.error(
f"{longer} is not a unique prefix: {similar}?"
) # pragma: no cover
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)
Expand Down Expand Up @@ -497,7 +495,7 @@ def parse_shorts(
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}"
) # pragma: no cover
)
left = token.lstrip("-")
parsed: list[Pattern] = []
while left != "":
Expand Down Expand Up @@ -564,8 +562,8 @@ def parse_shorts(
de_abbreviated = True
break
if len(similar) > 1:
raise tokens.error(
"%s is specified ambiguously %d times" % (short, len(similar))
raise DocoptLanguageError(
f"{short} is specified ambiguously {len(similar)} times"
)
elif len(similar) < 1:
o = Option(short, None, 0)
Expand Down
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@
requires = ['setuptools', 'wheel']

[tool.pytest.ini_options]
addopts = "--cov-report term-missing --cov-report xml --cov=docopt --mypy"
testpaths = [
"./tests",
]
testpaths = ["./tests"]
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ universal = 1
[flake8]
max-line-length = 88
extend-ignore = E203
extend-exclude = .*
19 changes: 19 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from pathlib import Path
import re
from typing import Generator, Sequence
from unittest import mock

import docopt
import pytest
Expand Down Expand Up @@ -96,3 +98,20 @@ def reportinfo(self):

class DocoptTestException(Exception):
pass


@pytest.fixture(autouse=True)
def override_sys_argv(argv: Sequence[str]) -> Generator[None, None, None]:
"""Patch `sys.argv` with a fixed value during tests.

A lot of docopt tests call docopt() without specifying argv, which uses
`sys.argv` by default, so a predictable value for it is necessary.
"""
with mock.patch("sys.argv", new=argv):
yield


@pytest.fixture
def argv() -> Sequence[str]:
"""The `sys.argv` value seen inside tests."""
return ["exampleprogram"]
158 changes: 120 additions & 38 deletions tests/test_docopt.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations, with_statement
from __future__ import annotations
from typing import Sequence
import re
from textwrap import dedent

from docopt import (
ParsedOptions,
docopt,
DocoptExit,
DocoptLanguageError,
Expand All @@ -18,8 +19,10 @@
lint_docstring,
parse_argv,
parse_docstring_sections,
parse_longer,
parse_options,
parse_pattern,
parse_shorts,
formal_usage,
Tokens,
transform,
Expand Down Expand Up @@ -87,7 +90,10 @@ def test_commands():
assert docopt("Usage: prog (add|rm)", "add") == {"add": True, "rm": False}
assert docopt("Usage: prog (add|rm)", "rm") == {"add": False, "rm": True}
assert docopt("Usage: prog a b", "a b") == {"a": True, "b": True}
with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments.*'b'.*'a'",
):
docopt("Usage: prog a b", "b a")


Expand Down Expand Up @@ -456,45 +462,81 @@ def test_pattern_fix_identities_2():
assert pattern.children[0].children[1] is pattern.children[1]


@pytest.mark.parametrize("tokens", [[], ["not_a_long_option"]])
def test_parse_longer__rejects_inappropriate_token(tokens: list[str]):
with raises(
ValueError, match=r"parse_longer got what appears to be an invalid token"
):
parse_longer(Tokens(tokens), [])


def test_parse_longer__rejects_duplicate_long_options():
options = [Option(None, "--foo"), Option(None, "--foo")]
with raises(DocoptLanguageError, match=r"foo is not a unique prefix"):
parse_longer(Tokens("--foo"), options)


@pytest.mark.parametrize("tokens", [[], ["not_a_short_option"]])
def test_parse_shorts__rejects_inappropriate_token(tokens: list[str]):
with raises(
ValueError, match=r"parse_shorts got what appears to be an invalid token"
):
parse_shorts(Tokens(tokens), [])


def test_parse_shorts__rejects_duplicate_short_options():
options = [Option("-f"), Option("-f")]
with raises(DocoptLanguageError, match=r"-f is specified ambiguously 2 times"):
parse_shorts(Tokens("-f"), options)


def test_long_options_error_handling():
# with raises(DocoptLanguageError):
# docopt('Usage: prog --non-existent', '--non-existent')
# with raises(DocoptLanguageError):
# docopt('Usage: prog --non-existent')
with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments.*--non-existent",
):
docopt("Usage: prog", "--non-existent")
with raises(DocoptExit):
with raises(
DocoptExit, match=r"Warning: found unmatched \(duplicate\?\) arguments.*--ver\b"
):
docopt(
"Usage: prog [--version --verbose]\n" "Options: --version\n --verbose",
"--ver",
)
with raises(DocoptLanguageError):
# --long is missing ARG in usage
with raises(DocoptLanguageError, match=r"unmatched '\('"):
docopt("Usage: prog --long\nOptions: --long ARG")
with raises(DocoptExit):
with raises(DocoptExit, match=r"--long requires argument"):
docopt("Usage: prog --long ARG\nOptions: --long ARG", "--long")
with raises(DocoptLanguageError):
with raises(DocoptLanguageError, match=r"--long must not have an argument"):
docopt("Usage: prog --long=ARG\nOptions: --long")
with raises(DocoptExit):
with raises(DocoptExit, match=r"--long must not have an argument"):
docopt("Usage: prog --long\nOptions: --long", "--long=ARG")


def test_short_options_error_handling():
with raises(DocoptLanguageError):
with raises(DocoptLanguageError, match=r"-x is specified ambiguously 2 times"):
docopt("Usage: prog -x\nOptions: -x this\n -x that")

with raises(DocoptExit):
with raises(
DocoptExit, match=r"Warning: found unmatched \(duplicate\?\) arguments.*-x"
):
docopt("Usage: prog", "-x")

with raises(DocoptLanguageError):
docopt("Usage: prog -o\nOptions: -o ARG")
with raises(DocoptExit):
with raises(DocoptExit, match=r"-o requires argument"):
docopt("Usage: prog -o ARG\nOptions: -o ARG", "-o")


def test_matching_paren():
with raises(DocoptLanguageError):
with raises(DocoptLanguageError, match=r"unmatched '\['"):
docopt("Usage: prog [a [b]")
with raises(DocoptLanguageError):
with raises(DocoptLanguageError, match=r"unexpected ending: '\)'"):
docopt("Usage: prog [a [b] ] c )")


Expand All @@ -509,11 +551,13 @@ def test_allow_double_dash():
"<arg>": "1",
"--": False,
}
with raises(DocoptExit): # "--" is not allowed; FIXME?
with raises(
DocoptExit, match=r"Warning: found unmatched \(duplicate\?\) arguments.*-o\b"
): # "--" is not allowed; FIXME?
docopt("usage: prog [-o] <arg>\noptions:-o", "-- -o")


def test_docopt():
def test_docopt(capsys: pytest.CaptureFixture):
doc = """Usage: prog [-v] A

Options: -v Be verbose."""
Expand Down Expand Up @@ -553,14 +597,37 @@ def test_docopt():
"OUTPUT": None,
}

with raises(DocoptExit): # does not match
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments.*output\.py",
):
docopt(doc, "-v input.py output.py")

with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments.*--fake",
):
docopt(doc, "--fake")

capsys.readouterr() # clear any captured output

with raises(SystemExit):
docopt(doc, "--hel")
assert capsys.readouterr().out.startswith("Usage: prog")


@pytest.mark.parametrize(
"items, expected",
[
({}, "{}"),
(
{"<z>": None, "<a>": None, "--foo": "abc", "--bar": True},
("{'--bar': True,\n '--foo': 'abc',\n '<a>': None,\n '<z>': None}"),
),
],
)
def test_docopt_result_dict_repr(items: dict[str, object], expected: str):
assert repr(ParsedOptions(items)) == expected


@pytest.mark.parametrize(
Expand Down Expand Up @@ -593,55 +660,75 @@ def test_docopt__usage_descriptions_cant_bridge_usage_section(


def test_language_errors():
with raises(DocoptLanguageError):
with raises(
DocoptLanguageError,
match=r'Failed to parse doc: "usage:" section \(case-insensitive\) not '
r"found\. Check http://docopt\.org/ for examples of how your doc "
r"should look\.",
):
docopt("no usage with colon here")
with raises(DocoptLanguageError):
with raises(
DocoptLanguageError, match=r'More than one "usage:" \(case-insensitive\)'
):
docopt("usage: here \n\n and again usage: here")


def test_issue_40():
def test_issue_40(capsys: pytest.CaptureFixture):
with raises(SystemExit): # i.e. shows help
docopt("usage: prog --help-commands | --help", "--help")
assert capsys.readouterr().out.startswith("usage: prog --help-commands | --help")

assert docopt("usage: prog --aabb | --aa", "--aa") == {
"--aabb": False,
"--aa": True,
}


def test_issue34_unicode_strings():
try:
assert docopt(eval("u'usage: prog [-o <a>]'"), "") == {"-o": False, "<a>": None}
except SyntaxError:
pass # Python 3


def test_count_multiple_flags():
assert docopt("usage: prog [-v]", "-v") == {"-v": True}
assert docopt("usage: prog [-vv]", "") == {"-v": 0}
assert docopt("usage: prog [-vv]", "-v") == {"-v": 1}
assert docopt("usage: prog [-vv]", "-vv") == {"-v": 2}
with raises(DocoptExit):
with raises(
DocoptExit, match=r"Warning: found unmatched \(duplicate\?\) arguments.*'-v'"
):
docopt("usage: prog [-vv]", "-vvv")
assert docopt("usage: prog [-v | -vv | -vvv]", "-vvv") == {"-v": 3}
assert docopt("usage: prog -v...", "-vvvvvv") == {"-v": 6}
assert docopt("usage: prog [--ver --ver]", "--ver --ver") == {"--ver": 2}


def test_any_options_parameter():
with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments"
r".*-f.*-o.*-o.*--bar.*--spam.*eggs",
):
docopt("usage: prog [options]", "-foo --bar --spam=eggs")
# assert docopt('usage: prog [options]', '-foo --bar --spam=eggs',
# any_options=True) == {'-f': True, '-o': 2,
# '--bar': True, '--spam': 'eggs'}
with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments"
r".*--foo.*--bar.*--bar",
):
docopt("usage: prog [options]", "--foo --bar --bar")
# assert docopt('usage: prog [options]', '--foo --bar --bar',
# any_options=True) == {'--foo': True, '--bar': 2}
with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments"
r".*--bar.*--bar.*--bar.*-f.*-f.*-f.*-f",
):
docopt("usage: prog [options]", "--bar --bar --bar -ffff")
# assert docopt('usage: prog [options]', '--bar --bar --bar -ffff',
# any_options=True) == {'--bar': 3, '-f': 4}
with raises(DocoptExit):
with raises(
DocoptExit,
match=r"Warning: found unmatched \(duplicate\?\) arguments"
r".*--long.*arg.*--long.*another",
):
docopt("usage: prog [options]", "--long=arg --long=another")


Expand Down Expand Up @@ -717,12 +804,7 @@ def test_issue_71_double_dash_is_not_a_valid_option_argument():
with raises(DocoptExit, match=r"--log requires argument"):
docopt("usage: prog [--log=LEVEL] [--] <args>...", "--log -- 1 2")
with raises(DocoptExit, match=r"-l requires argument"):
docopt(
"""\
usage: prog [-l LEVEL] [--] <args>...
options: -l LEVEL""",
"-l -- 1 2",
)
docopt("usage: prog [-l LEVEL] [--] <args>...\noptions: -l LEVEL", "-l -- 1 2")


option_examples: Sequence[tuple[str, Sequence[Option]]] = [
Expand Down
Loading