bpo-42797: Add `-m` option for `doctest`
Allow `doctest` to filter on specific tests and examples with a glob pattern and a list of indexes, eg: `python -m doctest my_module.py -m open_*_file -m save_file:2,4-7`
This commit is contained in:
parent
f4936ad1c4
commit
b88c0b39b1
|
@ -130,6 +130,29 @@ And so on, eventually ending with:
|
|||
Test passed.
|
||||
$
|
||||
|
||||
You can also limit the number of tests by passing a glob-like expression, and
|
||||
optionally the indexes of the test cases:
|
||||
|
||||
The following will only run tests for methods that end in 'my_method':
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python example.py -m my_method
|
||||
|
||||
The following will only run tests for methods in my module that end in
|
||||
'my_method':
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python example.py -m my_module.*.my_method
|
||||
|
||||
The following will only run tests for methods in my module that end in
|
||||
'my_method', and also only test cases 0, 1, 4, 5, 6, 10, and all the rest:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python example.py -m my_module.*.my_method:-1,4-6,10-
|
||||
|
||||
That's all you need to know to start making productive use of :mod:`doctest`!
|
||||
Jump in. The following sections provide full details. Note that there are many
|
||||
examples of doctests in the standard Python test suite and libraries.
|
||||
|
@ -288,6 +311,22 @@ strings are treated as if they were docstrings. In output, a key ``K`` in
|
|||
Any classes found are recursively searched similarly, to test docstrings in
|
||||
their contained methods and nested classes.
|
||||
|
||||
You can also filter the tests and test cases via ``-m``/``--match``:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python example.py -m my_method:4 -m ...
|
||||
|
||||
The first part is required and it is the suffix of the full test name (usually
|
||||
something like ``__main__.method_name`` or ``__main__.ClassName.method_name``),
|
||||
where you can also add ``*`` to match any substring, eg ``my_method``,
|
||||
``my_*_method``, ``my_module.*.my_method``. Since it's a suffix a ``*`` is
|
||||
implicitly added to the beginning of the pattern.
|
||||
|
||||
The second part is optional (along with ``:``) and it can be a list of 0-based
|
||||
indexes or range of indexes, eg ``1``, ``4-8``, ``-8``, ``4-``, or a mix of them
|
||||
``-3,8,12,15-``.
|
||||
|
||||
.. impl-detail::
|
||||
Prior to version 3.4, extension modules written in C were not fully
|
||||
searched by doctest.
|
||||
|
|
551
Lib/doctest.py
551
Lib/doctest.py
|
@ -93,6 +93,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
import __future__
|
||||
import copy
|
||||
import difflib
|
||||
import inspect
|
||||
import linecache
|
||||
|
@ -104,6 +105,8 @@ import traceback
|
|||
import unittest
|
||||
from io import StringIO
|
||||
from collections import namedtuple
|
||||
from types import ModuleType
|
||||
from typing import Pattern, List, Optional, Tuple, Union
|
||||
|
||||
TestResults = namedtuple('TestResults', 'failed attempted')
|
||||
|
||||
|
@ -846,7 +849,11 @@ class DocTestFinder:
|
|||
self._recurse = recurse
|
||||
self._exclude_empty = exclude_empty
|
||||
|
||||
def find(self, obj, name=None, module=None, globs=None, extraglobs=None):
|
||||
def find(self, obj, name: Optional[str]=None,
|
||||
module: Optional[ModuleType]=None, globs: Optional[dict]=None,
|
||||
extraglobs: Optional[dict]=None,
|
||||
filters_or_text: Optional[Union[str, List['DocTestFilter']]]=None)\
|
||||
-> List[DocTest]:
|
||||
"""
|
||||
Return a list of the DocTests that are defined by the given
|
||||
object's docstring, or by any of its contained objects'
|
||||
|
@ -880,6 +887,8 @@ class DocTestFinder:
|
|||
otherwise. If `extraglobs` is not specified, then it defaults
|
||||
to {}.
|
||||
|
||||
If `filters_or_text` is specified, the tests and examples are also
|
||||
filtered accordingly.
|
||||
"""
|
||||
# If name was not specified, then extract it from the object.
|
||||
if name is None:
|
||||
|
@ -946,8 +955,71 @@ class DocTestFinder:
|
|||
# <= 2.3 that got lost by accident in 2.4. It was repaired in
|
||||
# 2.4.4 and 2.5.
|
||||
tests.sort()
|
||||
|
||||
if filters_or_text:
|
||||
tests = self.filter_tests(tests, filters_or_text)
|
||||
|
||||
return tests
|
||||
|
||||
def filter_tests(self, tests: List[DocTest],
|
||||
filters_or_text: Union[str, List['DocTestFilter']]) \
|
||||
-> List[DocTest]:
|
||||
"""
|
||||
Filter collected tests according a user specification, by using
|
||||
`DocTestFilter`s.
|
||||
|
||||
:param tests: The list of tests to filter
|
||||
:param filters_or_text: Either a user-specified string, or a parsed one
|
||||
:return: A (not necessarily strict) subset of the passed in tests
|
||||
"""
|
||||
if isinstance(filters_or_text, str):
|
||||
filters_text = filters_or_text
|
||||
filters = DocTestFilterParser().parse_filters(filters_text)
|
||||
else:
|
||||
filters = filters_or_text
|
||||
|
||||
return list(filter(None, (
|
||||
self.filter_test(filters, test)
|
||||
for test in tests
|
||||
)))
|
||||
|
||||
def filter_test(self, filters, test: DocTest) -> Optional[DocTest]:
|
||||
"""
|
||||
Filter a test according the user specifications. If a test shouldn't be
|
||||
run `None` is returned. If it should be, and all of its examples should
|
||||
as well, it's returned as is. If only a few of its examples should be,
|
||||
a new test with only the appropriate examples is returned.
|
||||
|
||||
:param filters: The user-specified `DocTestFilter`s
|
||||
:param test: A test to either discard, accept it fully, or with a subset
|
||||
of its examples
|
||||
:return: Either `None` to discard the test, the test as is, or a copy
|
||||
with fewer examples
|
||||
"""
|
||||
matching_filters = [
|
||||
_filter
|
||||
for _filter in filters
|
||||
if _filter.matches_test(test)
|
||||
]
|
||||
if not matching_filters:
|
||||
return None
|
||||
examples = [
|
||||
example
|
||||
for index, example in enumerate(test.examples)
|
||||
if any(
|
||||
_filter.matches_example(test, example, index)
|
||||
for _filter in matching_filters
|
||||
)
|
||||
]
|
||||
if not examples:
|
||||
return None
|
||||
|
||||
if examples != test.examples:
|
||||
test = copy.copy(test)
|
||||
test.examples = examples
|
||||
|
||||
return test
|
||||
|
||||
def _from_module(self, module, object):
|
||||
"""
|
||||
Return true if the given object is defined in the given
|
||||
|
@ -1125,6 +1197,452 @@ class DocTestFinder:
|
|||
# We couldn't find the line number.
|
||||
return None
|
||||
|
||||
|
||||
class DocTestFilter:
|
||||
"""
|
||||
Used by `DocTestFinder` to limit which tests should be run. It's
|
||||
usually created by `DocTestFilterParser`, in response to a user-defined
|
||||
filter string.
|
||||
|
||||
It can filter in two ways:
|
||||
* on the full qualified name of the test (eg 'module.Class.method') via a
|
||||
regular expression
|
||||
* and also optionally via a list of 0-based text index ranges
|
||||
"""
|
||||
def __init__(self, name_regex: Pattern,
|
||||
number_ranges: Optional[List[range]]):
|
||||
"""
|
||||
|
||||
:param name_regex: A compiled regular expression from `re.compile`
|
||||
:param number_ranges: A list objects that supports the `in` operator.
|
||||
Normally this will be a list of `range` objects.
|
||||
"""
|
||||
self.name_regex = name_regex
|
||||
self.number_ranges = number_ranges
|
||||
|
||||
def matches_test(self, test: DocTest) -> bool:
|
||||
"""
|
||||
Check if a test should be run based only on the name.
|
||||
:param test: The `DocTest` object to check against
|
||||
:return: Whether it matches
|
||||
|
||||
>>> DocTestFilter(re.compile(r"^method$"), []).matches_test(
|
||||
... DocTest([], {}, "method", "file.py", 0, ""))
|
||||
True
|
||||
>>> DocTestFilter(re.compile(r"^method$"), []).matches_test(
|
||||
... DocTest([], {}, "another_method", "file.py", 0, ""))
|
||||
False
|
||||
>>> DocTestFilter(re.compile(r"^.*method$"), []).matches_test(
|
||||
... DocTest([], {}, "another_method", "file.py", 0, ""))
|
||||
True
|
||||
"""
|
||||
return bool(self.name_regex.match(test.name))
|
||||
|
||||
def matches_example(self, test: DocTest, example: Example, index: int
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a test's example should be run based on the example's index. It
|
||||
is assumed that `matches_test` has already been used to match the `test`
|
||||
object.
|
||||
|
||||
:param test: The `DocTest` object which contains `example`
|
||||
:param example: The `Example` object to check against
|
||||
:param index: The 0-based index of the `Example` within `test`
|
||||
:return: Whether it matches
|
||||
|
||||
>>> a_test = DocTest([], {}, "another_method", "file.py", 0, "")
|
||||
>>> DocTestFilter(re.compile(r"^.*method$"), None).matches_example(
|
||||
... a_test, Example("", ""), 0)
|
||||
True
|
||||
>>> DocTestFilter(re.compile(r"^.*method$"), [
|
||||
... range(5),
|
||||
... ]).matches_example(a_test, Example("", ""), 0)
|
||||
True
|
||||
>>> DocTestFilter(re.compile(r"^.*method$"), [
|
||||
... range(5),
|
||||
... ]).matches_example(a_test, Example("", ""), 5)
|
||||
False
|
||||
>>> DocTestFilter(re.compile(r"^.*method$"), [
|
||||
... range(5), range(10, 20), [5],
|
||||
... ]).matches_example(a_test, Example("", ""), 5)
|
||||
True
|
||||
>>> DocTestFilter(re.compile(r"^.*method$"), [
|
||||
... range(10), range(20), [5],
|
||||
... ]).matches_example(a_test, Example("", ""), 5)
|
||||
True
|
||||
"""
|
||||
if self.number_ranges is None:
|
||||
return True
|
||||
return any(
|
||||
index
|
||||
in number_range
|
||||
for number_range in self.number_ranges
|
||||
)
|
||||
|
||||
|
||||
class InvalidTestFilterException(Exception):
|
||||
"""
|
||||
Raised in `DocTestFilterParser.parse_filters` (and all other methods in it)
|
||||
to signify an invalid pattern.
|
||||
"""
|
||||
|
||||
|
||||
class DocTestFilterParser:
|
||||
"""
|
||||
Since we allow the user to pass a filter for selecting tests, this class
|
||||
helps with parsing that text, and converting it to a list `DocTestFilter`s.
|
||||
|
||||
It accepts filters with the following attributes:
|
||||
* Each filter must have a name suffix specifier
|
||||
* Name specifiers can have '*' inside them to match any sub-string
|
||||
* The name can not be empty or only '*'
|
||||
* Each filter can also optionally contain a set of indexes or ranges of
|
||||
indexes to limit which examples are run inside a test
|
||||
|
||||
Examples of understood patterns:
|
||||
* method
|
||||
* group_of_*_methods
|
||||
* specific.module.*.method
|
||||
* methods_with_optional_suffix*
|
||||
* method:5
|
||||
* method:2-5
|
||||
* method:2-
|
||||
* method:-5
|
||||
* method:-3,5-10,15-
|
||||
"""
|
||||
def parse_filters(self, filters_text: str) -> List[DocTestFilter]:
|
||||
"""
|
||||
Convert a user-provided filter text to a list of filters
|
||||
|
||||
:param filters_text: The user input
|
||||
:return: A list of `DocTestFilter` instances
|
||||
"""
|
||||
return [
|
||||
self.parse_filter(filter_text)
|
||||
for filter_text in self.split_filter_texts(filters_text)
|
||||
]
|
||||
|
||||
def split_filter_texts(self, filters_text: str) -> List[str]:
|
||||
"""
|
||||
Split the user input into multiple filters, to be parsed
|
||||
|
||||
:param filters_text: The user input
|
||||
:return: A list of strings that each should be converted to a
|
||||
`DocTestFilter`
|
||||
|
||||
>>> DocTestFilterParser().split_filter_texts("")
|
||||
[]
|
||||
>>> DocTestFilterParser().split_filter_texts(" " * 20)
|
||||
[]
|
||||
>>> DocTestFilterParser().split_filter_texts(" abc def ghi ")
|
||||
['abc', 'def', 'ghi']
|
||||
"""
|
||||
filters_text = filters_text.strip()
|
||||
if not filters_text:
|
||||
return []
|
||||
return re.split(r"\s+", filters_text)
|
||||
|
||||
def parse_filter(self, filter_text: str) -> DocTestFilter:
|
||||
"""
|
||||
Convert the text for a single filter to a `DocTestFilter`
|
||||
|
||||
:param filter_text: Part of the user input
|
||||
:return: `DocTestFilter`
|
||||
"""
|
||||
test_name_text, line_numbers_text = self.get_filter_parts(filter_text)
|
||||
return DocTestFilter(
|
||||
self.parse_test_name(test_name_text),
|
||||
self.parse_number_ranges(line_numbers_text),
|
||||
)
|
||||
|
||||
def get_filter_parts(self, filter_text: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Split the filter into two parts: the name specifier and the index
|
||||
specifier.
|
||||
|
||||
:param filter_text: Part of the user input
|
||||
:return: A tuple of the name specifier and the index specifier
|
||||
|
||||
>>> DocTestFilterParser().get_filter_parts("method")
|
||||
('method', '')
|
||||
>>> DocTestFilterParser().get_filter_parts("method:512,3,-7")
|
||||
('method', '512,3,-7')
|
||||
>>> DocTestFilterParser().get_filter_parts(":512,3,-7")
|
||||
('', '512,3,-7')
|
||||
>>> DocTestFilterParser().get_filter_parts(":")
|
||||
('', '')
|
||||
>>> DocTestFilterParser().get_filter_parts("method:512,3,-7:") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
"""
|
||||
filter_text = filter_text.strip()
|
||||
parts = filter_text.split(':')
|
||||
if len(parts) > 2:
|
||||
raise InvalidTestFilterException(
|
||||
f"A filter should have at most two parts, name and ranges, "
|
||||
f"not {len(parts)}: '{filter_text}'")
|
||||
|
||||
if len(parts) == 1:
|
||||
test_name_text, = parts
|
||||
numbers_text = ''
|
||||
else:
|
||||
test_name_text, numbers_text = parts
|
||||
|
||||
return test_name_text, numbers_text
|
||||
|
||||
def parse_test_name(self, test_name_text: str) -> Pattern:
|
||||
"""
|
||||
Convert a name speficier to a regex:
|
||||
* Escape the specifier so that it matches literally the name
|
||||
* Convert '*' to regex '.*' to allow to match substrings
|
||||
* The resulting regex should not be equivalent to '^.*$', ie it should
|
||||
match literally some part of the method
|
||||
|
||||
:param test_name_text: The name specifier, perhaps containing '*'
|
||||
:return: The compiled regex
|
||||
|
||||
>>> def test(pattern):
|
||||
... regex = DocTestFilterParser().parse_test_name(pattern)
|
||||
... return [
|
||||
... name
|
||||
... for name in [
|
||||
... f'{part}.{_type}.{prefix}{method}{suffix}'
|
||||
... for part in ['part_a', 'part_b']
|
||||
... for _type in ['TypeA', 'TypeB']
|
||||
... for method in ['method', 'function']
|
||||
... for suffix in ['', '_plus']
|
||||
... for prefix in ['', 'plus_']
|
||||
... ]
|
||||
... if regex.match(name)
|
||||
... ]
|
||||
>>> test("method") # doctest: +NORMALIZE_WHITESPACE
|
||||
['part_a.TypeA.method', 'part_a.TypeA.plus_method',
|
||||
'part_a.TypeB.method', 'part_a.TypeB.plus_method',
|
||||
'part_b.TypeA.method', 'part_b.TypeA.plus_method',
|
||||
'part_b.TypeB.method', 'part_b.TypeB.plus_method']
|
||||
>>> test("method*") # doctest: +NORMALIZE_WHITESPACE
|
||||
['part_a.TypeA.method', 'part_a.TypeA.plus_method',
|
||||
'part_a.TypeA.method_plus', 'part_a.TypeA.plus_method_plus',
|
||||
'part_a.TypeB.method', 'part_a.TypeB.plus_method',
|
||||
'part_a.TypeB.method_plus', 'part_a.TypeB.plus_method_plus',
|
||||
'part_b.TypeA.method', 'part_b.TypeA.plus_method',
|
||||
'part_b.TypeA.method_plus', 'part_b.TypeA.plus_method_plus',
|
||||
'part_b.TypeB.method', 'part_b.TypeB.plus_method',
|
||||
'part_b.TypeB.method_plus', 'part_b.TypeB.plus_method_plus']
|
||||
>>> test("part_a.*method") # doctest: +NORMALIZE_WHITESPACE
|
||||
['part_a.TypeA.method', 'part_a.TypeA.plus_method',
|
||||
'part_a.TypeB.method', 'part_a.TypeB.plus_method']
|
||||
>>> test("part_a.*method*") # doctest: +NORMALIZE_WHITESPACE
|
||||
['part_a.TypeA.method', 'part_a.TypeA.plus_method',
|
||||
'part_a.TypeA.method_plus', 'part_a.TypeA.plus_method_plus',
|
||||
'part_a.TypeB.method', 'part_a.TypeB.plus_method',
|
||||
'part_a.TypeB.method_plus', 'part_a.TypeB.plus_method_plus']
|
||||
>>> test("") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
"""
|
||||
test_name_text = test_name_text.strip()
|
||||
if not test_name_text.replace("*", ""):
|
||||
raise InvalidTestFilterException(
|
||||
f"You need to specify at least some part of the test name")
|
||||
parts = re.split(r'\*+', '*' + test_name_text)
|
||||
escaped_parts = map(re.escape, parts)
|
||||
return re.compile(f"{'.*'.join(escaped_parts)}$")
|
||||
|
||||
def parse_number_ranges(self, number_ranges_text: str) -> List[range]:
|
||||
"""
|
||||
Convert an index specifier to a list of `range`s, to allow matching
|
||||
specific examples. An empty specifier returns one range that should
|
||||
match any index.
|
||||
|
||||
Examples:
|
||||
* '1': Match a specific example
|
||||
* '2-5': Match examples 2 to 5, inclusive
|
||||
* '-5': Up to 5, inclusive
|
||||
* '2-': From 2 to the end, inclusive
|
||||
* '-' or '': Match all examples
|
||||
* '-3,5-10,20-': A combination of the above
|
||||
|
||||
:param number_ranges_text: The index specifier for a test
|
||||
:return: A list of ranges to match the index of examples
|
||||
|
||||
>>> def test(pattern):
|
||||
... result = DocTestFilterParser().parse_number_ranges(pattern)
|
||||
... return sorted(set(sum(map(list, result), [])))[:1010]
|
||||
>>> test("") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 1000, 1001, ...]
|
||||
>>> test("-") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 1000, 1001, ...]
|
||||
>>> test("512") # doctest: +ELLIPSIS
|
||||
[512]
|
||||
>>> test("-512") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 510, 511, 512]
|
||||
>>> test("512-") # doctest: +ELLIPSIS
|
||||
[512, 513, 514, ..., 1000, 1001, ...]
|
||||
>>> test("256-512") # doctest: +ELLIPSIS
|
||||
[256, 257, 258, ..., 510, 511, 512]
|
||||
>>> test("512-256") # doctest: +ELLIPSIS
|
||||
[]
|
||||
>>> test("256-512-768") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("0xf") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("5-abc") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("1,5,7") # doctest: +ELLIPSIS
|
||||
[1, 5, 7]
|
||||
>>> test("1,10-20,7") # doctest: +ELLIPSIS
|
||||
[1, 7, 10, 11, 12, ..., 18, 19, 20]
|
||||
>>> test("1,20-10,7") # doctest: +ELLIPSIS
|
||||
[1, 7]
|
||||
>>> test("1,20-10,7,10,11") # doctest: +ELLIPSIS
|
||||
[1, 7, 10, 11]
|
||||
>>> test("1,10-20,-5") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, 3, 4, 5, 10, 11, 12, ..., 18, 19, 20]
|
||||
>>> test("600-,10-20,-5") # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
[0, 1, 2, 3, 4, 5, 10, 11, 12, ..., 18, 19, 20, 600, 601, 602,
|
||||
..., 1000, 1001, ...]
|
||||
>>> test("600-,10-20,,-5") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 1000, 1001, ...]
|
||||
>>> test("600-,10-20-40,,-5") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("600-,10-0xf,,-5") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("600-,10-abc,,-5") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
"""
|
||||
return [
|
||||
self.parse_number_range(number_range_text)
|
||||
for number_range_text in number_ranges_text.split(',')
|
||||
]
|
||||
|
||||
def parse_number_range(self, number_range_text: str) -> range:
|
||||
"""
|
||||
Convert a part of an index specifier to a list of `range`s, to allow
|
||||
matching specific examples. An empty specifier returns a range that
|
||||
should match any index.
|
||||
|
||||
Examples:
|
||||
* '1': Match a specific example
|
||||
* '2-5': Match examples 2 to 5, inclusive
|
||||
* '-5': Up to 5, inclusive
|
||||
* '2-': From 2 to the end, inclusive
|
||||
* '-' or '': Match all examples
|
||||
|
||||
:param number_range_text: A part of an index specifier
|
||||
:return: A range that matches the respective example indexes
|
||||
|
||||
>>> def test(pattern):
|
||||
... result = DocTestFilterParser().parse_number_range(pattern)
|
||||
... return sorted(result)[:1010]
|
||||
>>> test("") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 1000, 1001, ...]
|
||||
>>> test("-") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 1000, 1001, ...]
|
||||
>>> test("512") # doctest: +ELLIPSIS
|
||||
[512]
|
||||
>>> test("-512") # doctest: +ELLIPSIS
|
||||
[0, 1, 2, ..., 510, 511, 512]
|
||||
>>> test("512-") # doctest: +ELLIPSIS
|
||||
[512, 513, 514, ..., 1000, 1001, ...]
|
||||
>>> test("256-512") # doctest: +ELLIPSIS
|
||||
[256, 257, 258, ..., 510, 511, 512]
|
||||
>>> test("512-256") # doctest: +ELLIPSIS
|
||||
[]
|
||||
>>> test("256-512-768") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("0xf") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> test("5-abc") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
"""
|
||||
number_range_text = number_range_text.strip()
|
||||
parts = number_range_text.split('-')
|
||||
if len(parts) > 2:
|
||||
raise InvalidTestFilterException(
|
||||
f"Number ranges must be either a single number "
|
||||
f"(eg '512'), or a range either without start (eg '-512'), "
|
||||
f"without end (eg '512-') or with just start and end "
|
||||
f"(eg '256-512), not with more parts: "
|
||||
f"'{number_range_text}'")
|
||||
|
||||
if len(parts) == 1:
|
||||
number_text, = parts
|
||||
if not number_text:
|
||||
start = 0
|
||||
end = 10000
|
||||
else:
|
||||
start = end = self.parse_number(number_text)
|
||||
else:
|
||||
start_number_text, end_number_text = parts
|
||||
if not start_number_text and not end_number_text:
|
||||
start = 0
|
||||
end = 10000
|
||||
elif not start_number_text:
|
||||
start = 0
|
||||
end = self.parse_number(end_number_text)
|
||||
elif not end_number_text:
|
||||
start = self.parse_number(start_number_text)
|
||||
end = 10000
|
||||
else:
|
||||
start = self.parse_number(start_number_text)
|
||||
end = self.parse_number(end_number_text)
|
||||
|
||||
return range(start, end + 1)
|
||||
|
||||
def parse_number(self, number_text: str) -> int:
|
||||
"""
|
||||
A convenient method to raise an appropriate error if the number to parse
|
||||
was not a non-negative integer
|
||||
|
||||
:param number_text: A number string to parse
|
||||
:return: The parsed number
|
||||
|
||||
>>> DocTestFilterParser().parse_number("0")
|
||||
0
|
||||
>>> DocTestFilterParser().parse_number("512")
|
||||
512
|
||||
>>> DocTestFilterParser().parse_number("-4") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
>>> DocTestFilterParser().parse_number("0xf") # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
doctest.InvalidTestFilterException: ...
|
||||
"""
|
||||
try:
|
||||
number = int(number_text)
|
||||
except ValueError:
|
||||
number = None
|
||||
|
||||
if number is None or number < 0:
|
||||
raise InvalidTestFilterException(
|
||||
f"Line numbers must be positive integers, not "
|
||||
f"'{number_text}'")
|
||||
|
||||
return number
|
||||
|
||||
######################################################################
|
||||
## 5. DocTest Runner
|
||||
######################################################################
|
||||
|
@ -1867,7 +2385,8 @@ master = None
|
|||
|
||||
def testmod(m=None, name=None, globs=None, verbose=None,
|
||||
report=True, optionflags=0, extraglobs=None,
|
||||
raise_on_error=False, exclude_empty=False):
|
||||
raise_on_error=False, exclude_empty=False, finder=None,
|
||||
filters_or_text=None):
|
||||
"""m=None, name=None, globs=None, verbose=None, report=True,
|
||||
optionflags=0, extraglobs=None, raise_on_error=False,
|
||||
exclude_empty=False
|
||||
|
@ -1923,6 +2442,13 @@ def testmod(m=None, name=None, globs=None, verbose=None,
|
|||
first unexpected exception or failure. This allows failures to be
|
||||
post-mortem debugged.
|
||||
|
||||
Optional keyword arg "finder" specifies a finder instance to use, besdies
|
||||
the default `DocTestFinder`.
|
||||
|
||||
Optional keyword arg "filters_or_text" specifies either the raw
|
||||
user-specified filter text, or the parsed version, in order to limit which
|
||||
tests and examples to run.
|
||||
|
||||
Advanced tomfoolery: testmod runs methods of a local instance of
|
||||
class doctest.Tester, then merges the results into (or creates)
|
||||
global Tester instance doctest.master. Methods of doctest.master
|
||||
|
@ -1949,14 +2475,18 @@ def testmod(m=None, name=None, globs=None, verbose=None,
|
|||
name = m.__name__
|
||||
|
||||
# Find, parse, and run all tests in the given module.
|
||||
finder = DocTestFinder(exclude_empty=exclude_empty)
|
||||
if finder is None:
|
||||
finder = DocTestFinder(exclude_empty=exclude_empty)
|
||||
|
||||
if raise_on_error:
|
||||
runner = DebugRunner(verbose=verbose, optionflags=optionflags)
|
||||
else:
|
||||
runner = DocTestRunner(verbose=verbose, optionflags=optionflags)
|
||||
|
||||
for test in finder.find(m, name, globs=globs, extraglobs=extraglobs):
|
||||
tests = finder.find(
|
||||
m, name, globs=globs, extraglobs=extraglobs,
|
||||
filters_or_text=filters_or_text)
|
||||
for test in tests:
|
||||
runner.run(test)
|
||||
|
||||
if report:
|
||||
|
@ -2759,6 +3289,10 @@ def _test():
|
|||
help=('specify a doctest option flag to apply'
|
||||
' to the test run; may be specified more'
|
||||
' than once to apply multiple options'))
|
||||
parser.add_argument('-m', '--match', action='append', default=[],
|
||||
help=('specify which tests and examples to run; may be '
|
||||
'specified more than once to select multiple'
|
||||
' methods'))
|
||||
parser.add_argument('-f', '--fail-fast', action='store_true',
|
||||
help=('stop running tests after first failure (this'
|
||||
' is a shorthand for -o FAIL_FAST, and is'
|
||||
|
@ -2775,6 +3309,10 @@ def _test():
|
|||
options |= OPTIONFLAGS_BY_NAME[option]
|
||||
if args.fail_fast:
|
||||
options |= FAIL_FAST
|
||||
if args.match:
|
||||
filters_text = " ".join(args.match)
|
||||
else:
|
||||
filters_text = None
|
||||
for filename in testfiles:
|
||||
if filename.endswith(".py"):
|
||||
# It is a module -- insert its dir into sys.path and try to
|
||||
|
@ -2784,10 +3322,11 @@ def _test():
|
|||
sys.path.insert(0, dirname)
|
||||
m = __import__(filename[:-3])
|
||||
del sys.path[0]
|
||||
failures, _ = testmod(m, verbose=verbose, optionflags=options)
|
||||
failures, _ = testmod(m, verbose=verbose, optionflags=options,
|
||||
filters_or_text=filters_text)
|
||||
else:
|
||||
failures, _ = testfile(filename, module_relative=False,
|
||||
verbose=verbose, optionflags=options)
|
||||
verbose=verbose, optionflags=options)
|
||||
if failures:
|
||||
return 1
|
||||
return 0
|
||||
|
|
|
@ -7,6 +7,7 @@ import sys
|
|||
from textwrap import dedent
|
||||
from types import FunctionType, MethodType, BuiltinFunctionType
|
||||
import pyclbr
|
||||
from typing import _SpecialForm
|
||||
from unittest import TestCase, main as unittest_main
|
||||
from test.test_importlib import util as test_importlib_util
|
||||
|
||||
|
@ -87,7 +88,8 @@ class PyclbrTest(TestCase):
|
|||
self.assertHasattr(module, name, ignore)
|
||||
py_item = getattr(module, name)
|
||||
if isinstance(value, pyclbr.Function):
|
||||
self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))
|
||||
self.assertIsInstance(py_item, (
|
||||
FunctionType, BuiltinFunctionType, _SpecialForm))
|
||||
if py_item.__module__ != moduleName:
|
||||
continue # skip functions that came from somewhere else
|
||||
self.assertEqual(py_item.__module__, value.module)
|
||||
|
|
|
@ -114,6 +114,7 @@ Des Barry
|
|||
Emanuel Barry
|
||||
Ulf Bartelt
|
||||
Campbell Barton
|
||||
Costas Basdekis
|
||||
Don Bashford
|
||||
Pior Bastida
|
||||
Nick Bastin
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Add a ``-m``/``--match`` option to the :mod:`doctest` module to filter which
|
||||
tests and examples should be run.
|
Loading…
Reference in New Issue