##// END OF EJS Templates
Try to fix Pytest error on Python 3.13 (#14287)
M Bussonnier -
r28582:001109ab merge
parent child Browse files
Show More
@@ -1,98 +1,98 b''
1 name: Run tests
1 name: Run tests
2
2
3 on:
3 on:
4 push:
4 push:
5 branches:
5 branches:
6 - main
6 - main
7 - '*.x'
7 - '*.x'
8 pull_request:
8 pull_request:
9 # Run weekly on Monday at 1:23 UTC
9 # Run weekly on Monday at 1:23 UTC
10 schedule:
10 schedule:
11 - cron: '23 1 * * 1'
11 - cron: '23 1 * * 1'
12 workflow_dispatch:
12 workflow_dispatch:
13
13
14
14
15 jobs:
15 jobs:
16 test:
16 test:
17 runs-on: ${{ matrix.os }}
17 runs-on: ${{ matrix.os }}
18 strategy:
18 strategy:
19 fail-fast: false
19 fail-fast: false
20 matrix:
20 matrix:
21 os: [ubuntu-latest, windows-latest]
21 os: [ubuntu-latest, windows-latest]
22 python-version: ["3.10", "3.11", "3.12"]
22 python-version: ["3.10", "3.11", "3.12"]
23 deps: [test_extra]
23 deps: [test_extra]
24 # Test all on ubuntu, test ends on macos
24 # Test all on ubuntu, test ends on macos
25 include:
25 include:
26 - os: macos-latest
26 - os: macos-latest
27 python-version: "3.10"
27 python-version: "3.10"
28 deps: test_extra
28 deps: test_extra
29 - os: macos-latest
29 - os: macos-latest
30 python-version: "3.11"
30 python-version: "3.11"
31 deps: test_extra
31 deps: test_extra
32 # Tests minimal dependencies set
32 # Tests minimal dependencies set
33 - os: ubuntu-latest
33 - os: ubuntu-latest
34 python-version: "3.11"
34 python-version: "3.11"
35 deps: test
35 deps: test
36 # Tests latest development Python version
36 # Tests latest development Python version
37 - os: ubuntu-latest
37 - os: ubuntu-latest
38 python-version: "3.13-dev"
38 python-version: "3.13-dev"
39 deps: test
39 deps: test
40 # Installing optional dependencies stuff takes ages on PyPy
40 # Installing optional dependencies stuff takes ages on PyPy
41 - os: ubuntu-latest
41 - os: ubuntu-latest
42 python-version: "pypy-3.10"
42 python-version: "pypy-3.10"
43 deps: test
43 deps: test
44 - os: windows-latest
44 - os: windows-latest
45 python-version: "pypy-3.10"
45 python-version: "pypy-3.10"
46 deps: test
46 deps: test
47 - os: macos-latest
47 - os: macos-latest
48 python-version: "pypy-3.10"
48 python-version: "pypy-3.10"
49 deps: test
49 deps: test
50
50
51 steps:
51 steps:
52 - uses: actions/checkout@v3
52 - uses: actions/checkout@v3
53 - name: Set up Python ${{ matrix.python-version }}
53 - name: Set up Python ${{ matrix.python-version }}
54 uses: actions/setup-python@v4
54 uses: actions/setup-python@v4
55 with:
55 with:
56 python-version: ${{ matrix.python-version }}
56 python-version: ${{ matrix.python-version }}
57 cache: pip
57 cache: pip
58 cache-dependency-path: |
58 cache-dependency-path: |
59 setup.cfg
59 setup.cfg
60 - name: Install latex
60 - name: Install latex
61 if: runner.os == 'Linux' && matrix.deps == 'test_extra'
61 if: runner.os == 'Linux' && matrix.deps == 'test_extra'
62 run: echo "disable latex for now, issues in mirros" #sudo apt-get -yq -o Acquire::Retries=3 --no-install-suggests --no-install-recommends install texlive dvipng
62 run: echo "disable latex for now, issues in mirros" #sudo apt-get -yq -o Acquire::Retries=3 --no-install-suggests --no-install-recommends install texlive dvipng
63 - name: Install and update Python dependencies (binary only)
63 - name: Install and update Python dependencies (binary only)
64 if: ${{ ! contains( matrix.python-version, 'dev' ) }}
64 if: ${{ ! contains( matrix.python-version, 'dev' ) }}
65 run: |
65 run: |
66 python -m pip install --only-binary ':all:' --upgrade pip setuptools wheel build
66 python -m pip install --only-binary ':all:' --upgrade pip setuptools wheel build
67 python -m pip install --only-binary ':all:' --no-binary curio --upgrade -e .[${{ matrix.deps }}]
67 python -m pip install --only-binary ':all:' --no-binary curio --upgrade -e .[${{ matrix.deps }}]
68 python -m pip install --only-binary ':all:' --upgrade check-manifest pytest-cov pytest-json-report
68 python -m pip install --only-binary ':all:' --upgrade check-manifest pytest-cov pytest-json-report 'pytest<8'
69 - name: Install and update Python dependencies (dev?)
69 - name: Install and update Python dependencies (dev?)
70 if: ${{ contains( matrix.python-version, 'dev' ) }}
70 if: ${{ contains( matrix.python-version, 'dev' ) }}
71 run: |
71 run: |
72 python -m pip install --pre --upgrade pip setuptools wheel build
72 python -m pip install --pre --upgrade pip setuptools wheel build
73 python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --no-binary curio --upgrade -e .[${{ matrix.deps }}]
73 python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --no-binary curio --upgrade -e .[${{ matrix.deps }}]
74 python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --upgrade check-manifest pytest-cov pytest-json-report
74 python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --upgrade check-manifest pytest-cov pytest-json-report
75 - name: Try building with Python build
75 - name: Try building with Python build
76 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
76 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
77 run: |
77 run: |
78 python -m build
78 python -m build
79 shasum -a 256 dist/*
79 shasum -a 256 dist/*
80 - name: Check manifest
80 - name: Check manifest
81 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
81 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
82 run: check-manifest
82 run: check-manifest
83 - name: pytest
83 - name: pytest
84 env:
84 env:
85 COLUMNS: 120
85 COLUMNS: 120
86 run: |
86 run: |
87 pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} --json-report --json-report-file=./report-${{ matrix.python-version }}-${{runner.os}}.json
87 pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} --json-report --json-report-file=./report-${{ matrix.python-version }}-${{runner.os}}.json --maxfail=15
88 - uses: actions/upload-artifact@v3
88 - uses: actions/upload-artifact@v3
89 with:
89 with:
90 name: upload pytest timing reports as json
90 name: upload pytest timing reports as json
91 path: |
91 path: |
92 ./report-*.json
92 ./report-*.json
93
93
94 - name: Upload coverage to Codecov
94 - name: Upload coverage to Codecov
95 uses: codecov/codecov-action@v3
95 uses: codecov/codecov-action@v3
96 with:
96 with:
97 name: Test
97 name: Test
98 files: /home/runner/work/ipython/ipython/coverage.xml
98 files: /home/runner/work/ipython/ipython/coverage.xml
@@ -1,178 +1,186 b''
1 """Experimental code for cleaner support of IPython syntax with unittest.
1 """Experimental code for cleaner support of IPython syntax with unittest.
2
2
3 In IPython up until 0.10, we've used very hacked up nose machinery for running
3 In IPython up until 0.10, we've used very hacked up nose machinery for running
4 tests with IPython special syntax, and this has proved to be extremely slow.
4 tests with IPython special syntax, and this has proved to be extremely slow.
5 This module provides decorators to try a different approach, stemming from a
5 This module provides decorators to try a different approach, stemming from a
6 conversation Brian and I (FP) had about this problem Sept/09.
6 conversation Brian and I (FP) had about this problem Sept/09.
7
7
8 The goal is to be able to easily write simple functions that can be seen by
8 The goal is to be able to easily write simple functions that can be seen by
9 unittest as tests, and ultimately for these to support doctests with full
9 unittest as tests, and ultimately for these to support doctests with full
10 IPython syntax. Nose already offers this based on naming conventions and our
10 IPython syntax. Nose already offers this based on naming conventions and our
11 hackish plugins, but we are seeking to move away from nose dependencies if
11 hackish plugins, but we are seeking to move away from nose dependencies if
12 possible.
12 possible.
13
13
14 This module follows a different approach, based on decorators.
14 This module follows a different approach, based on decorators.
15
15
16 - A decorator called @ipdoctest can mark any function as having a docstring
16 - A decorator called @ipdoctest can mark any function as having a docstring
17 that should be viewed as a doctest, but after syntax conversion.
17 that should be viewed as a doctest, but after syntax conversion.
18
18
19 Authors
19 Authors
20 -------
20 -------
21
21
22 - Fernando Perez <Fernando.Perez@berkeley.edu>
22 - Fernando Perez <Fernando.Perez@berkeley.edu>
23 """
23 """
24
24
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Copyright (C) 2009-2011 The IPython Development Team
27 # Copyright (C) 2009-2011 The IPython Development Team
28 #
28 #
29 # Distributed under the terms of the BSD License. The full license is in
29 # Distributed under the terms of the BSD License. The full license is in
30 # the file COPYING, distributed as part of this software.
30 # the file COPYING, distributed as part of this software.
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Imports
34 # Imports
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 # Stdlib
37 # Stdlib
38 import re
38 import re
39 import sys
39 import unittest
40 import unittest
40 from doctest import DocTestFinder, DocTestRunner, TestResults
41 from doctest import DocTestFinder, DocTestRunner, TestResults
41 from IPython.terminal.interactiveshell import InteractiveShell
42 from IPython.terminal.interactiveshell import InteractiveShell
42
43
43 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
44 # Classes and functions
45 # Classes and functions
45 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
46
47
47 def count_failures(runner):
48 def count_failures(runner):
48 """Count number of failures in a doctest runner.
49 """Count number of failures in a doctest runner.
49
50
50 Code modeled after the summarize() method in doctest.
51 Code modeled after the summarize() method in doctest.
51 """
52 """
52 return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0 ]
53 if sys.version_info < (3, 13):
54 return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0]
55 else:
56 return [
57 TestResults(failure, try_)
58 for failure, try_, skip in runner._stats.values()
59 if failure > 0
60 ]
53
61
54
62
55 class IPython2PythonConverter(object):
63 class IPython2PythonConverter(object):
56 """Convert IPython 'syntax' to valid Python.
64 """Convert IPython 'syntax' to valid Python.
57
65
58 Eventually this code may grow to be the full IPython syntax conversion
66 Eventually this code may grow to be the full IPython syntax conversion
59 implementation, but for now it only does prompt conversion."""
67 implementation, but for now it only does prompt conversion."""
60
68
61 def __init__(self):
69 def __init__(self):
62 self.rps1 = re.compile(r'In\ \[\d+\]: ')
70 self.rps1 = re.compile(r'In\ \[\d+\]: ')
63 self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
71 self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
64 self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
72 self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
65 self.pyps1 = '>>> '
73 self.pyps1 = '>>> '
66 self.pyps2 = '... '
74 self.pyps2 = '... '
67 self.rpyps1 = re.compile (r'(\s*%s)(.*)$' % self.pyps1)
75 self.rpyps1 = re.compile (r'(\s*%s)(.*)$' % self.pyps1)
68 self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2)
76 self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2)
69
77
70 def __call__(self, ds):
78 def __call__(self, ds):
71 """Convert IPython prompts to python ones in a string."""
79 """Convert IPython prompts to python ones in a string."""
72 from . import globalipapp
80 from . import globalipapp
73
81
74 pyps1 = '>>> '
82 pyps1 = '>>> '
75 pyps2 = '... '
83 pyps2 = '... '
76 pyout = ''
84 pyout = ''
77
85
78 dnew = ds
86 dnew = ds
79 dnew = self.rps1.sub(pyps1, dnew)
87 dnew = self.rps1.sub(pyps1, dnew)
80 dnew = self.rps2.sub(pyps2, dnew)
88 dnew = self.rps2.sub(pyps2, dnew)
81 dnew = self.rout.sub(pyout, dnew)
89 dnew = self.rout.sub(pyout, dnew)
82 ip = InteractiveShell.instance()
90 ip = InteractiveShell.instance()
83
91
84 # Convert input IPython source into valid Python.
92 # Convert input IPython source into valid Python.
85 out = []
93 out = []
86 newline = out.append
94 newline = out.append
87 for line in dnew.splitlines():
95 for line in dnew.splitlines():
88
96
89 mps1 = self.rpyps1.match(line)
97 mps1 = self.rpyps1.match(line)
90 if mps1 is not None:
98 if mps1 is not None:
91 prompt, text = mps1.groups()
99 prompt, text = mps1.groups()
92 newline(prompt+ip.prefilter(text, False))
100 newline(prompt+ip.prefilter(text, False))
93 continue
101 continue
94
102
95 mps2 = self.rpyps2.match(line)
103 mps2 = self.rpyps2.match(line)
96 if mps2 is not None:
104 if mps2 is not None:
97 prompt, text = mps2.groups()
105 prompt, text = mps2.groups()
98 newline(prompt+ip.prefilter(text, True))
106 newline(prompt+ip.prefilter(text, True))
99 continue
107 continue
100
108
101 newline(line)
109 newline(line)
102 newline('') # ensure a closing newline, needed by doctest
110 newline('') # ensure a closing newline, needed by doctest
103 #print "PYSRC:", '\n'.join(out) # dbg
111 #print "PYSRC:", '\n'.join(out) # dbg
104 return '\n'.join(out)
112 return '\n'.join(out)
105
113
106 #return dnew
114 #return dnew
107
115
108
116
109 class Doc2UnitTester(object):
117 class Doc2UnitTester(object):
110 """Class whose instances act as a decorator for docstring testing.
118 """Class whose instances act as a decorator for docstring testing.
111
119
112 In practice we're only likely to need one instance ever, made below (though
120 In practice we're only likely to need one instance ever, made below (though
113 no attempt is made at turning it into a singleton, there is no need for
121 no attempt is made at turning it into a singleton, there is no need for
114 that).
122 that).
115 """
123 """
116 def __init__(self, verbose=False):
124 def __init__(self, verbose=False):
117 """New decorator.
125 """New decorator.
118
126
119 Parameters
127 Parameters
120 ----------
128 ----------
121
129
122 verbose : boolean, optional (False)
130 verbose : boolean, optional (False)
123 Passed to the doctest finder and runner to control verbosity.
131 Passed to the doctest finder and runner to control verbosity.
124 """
132 """
125 self.verbose = verbose
133 self.verbose = verbose
126 # We can reuse the same finder for all instances
134 # We can reuse the same finder for all instances
127 self.finder = DocTestFinder(verbose=verbose, recurse=False)
135 self.finder = DocTestFinder(verbose=verbose, recurse=False)
128
136
129 def __call__(self, func):
137 def __call__(self, func):
130 """Use as a decorator: doctest a function's docstring as a unittest.
138 """Use as a decorator: doctest a function's docstring as a unittest.
131
139
132 This version runs normal doctests, but the idea is to make it later run
140 This version runs normal doctests, but the idea is to make it later run
133 ipython syntax instead."""
141 ipython syntax instead."""
134
142
135 # Capture the enclosing instance with a different name, so the new
143 # Capture the enclosing instance with a different name, so the new
136 # class below can see it without confusion regarding its own 'self'
144 # class below can see it without confusion regarding its own 'self'
137 # that will point to the test instance at runtime
145 # that will point to the test instance at runtime
138 d2u = self
146 d2u = self
139
147
140 # Rewrite the function's docstring to have python syntax
148 # Rewrite the function's docstring to have python syntax
141 if func.__doc__ is not None:
149 if func.__doc__ is not None:
142 func.__doc__ = ip2py(func.__doc__)
150 func.__doc__ = ip2py(func.__doc__)
143
151
144 # Now, create a tester object that is a real unittest instance, so
152 # Now, create a tester object that is a real unittest instance, so
145 # normal unittest machinery (or Nose, or Trial) can find it.
153 # normal unittest machinery (or Nose, or Trial) can find it.
146 class Tester(unittest.TestCase):
154 class Tester(unittest.TestCase):
147 def test(self):
155 def test(self):
148 # Make a new runner per function to be tested
156 # Make a new runner per function to be tested
149 runner = DocTestRunner(verbose=d2u.verbose)
157 runner = DocTestRunner(verbose=d2u.verbose)
150 for the_test in d2u.finder.find(func, func.__name__):
158 for the_test in d2u.finder.find(func, func.__name__):
151 runner.run(the_test)
159 runner.run(the_test)
152 failed = count_failures(runner)
160 failed = count_failures(runner)
153 if failed:
161 if failed:
154 # Since we only looked at a single function's docstring,
162 # Since we only looked at a single function's docstring,
155 # failed should contain at most one item. More than that
163 # failed should contain at most one item. More than that
156 # is a case we can't handle and should error out on
164 # is a case we can't handle and should error out on
157 if len(failed) > 1:
165 if len(failed) > 1:
158 err = "Invalid number of test results: %s" % failed
166 err = "Invalid number of test results: %s" % failed
159 raise ValueError(err)
167 raise ValueError(err)
160 # Report a normal failure.
168 # Report a normal failure.
161 self.fail('failed doctests: %s' % str(failed[0]))
169 self.fail('failed doctests: %s' % str(failed[0]))
162
170
163 # Rename it so test reports have the original signature.
171 # Rename it so test reports have the original signature.
164 Tester.__name__ = func.__name__
172 Tester.__name__ = func.__name__
165 return Tester
173 return Tester
166
174
167
175
168 def ipdocstring(func):
176 def ipdocstring(func):
169 """Change the function docstring via ip2py.
177 """Change the function docstring via ip2py.
170 """
178 """
171 if func.__doc__ is not None:
179 if func.__doc__ is not None:
172 func.__doc__ = ip2py(func.__doc__)
180 func.__doc__ = ip2py(func.__doc__)
173 return func
181 return func
174
182
175
183
176 # Make an instance of the classes for public use
184 # Make an instance of the classes for public use
177 ipdoctest = Doc2UnitTester()
185 ipdoctest = Doc2UnitTester()
178 ip2py = IPython2PythonConverter()
186 ip2py = IPython2PythonConverter()
@@ -1,859 +1,863 b''
1 # Based on Pytest doctest.py
1 # Based on Pytest doctest.py
2 # Original license:
2 # Original license:
3 # The MIT License (MIT)
3 # The MIT License (MIT)
4 #
4 #
5 # Copyright (c) 2004-2021 Holger Krekel and others
5 # Copyright (c) 2004-2021 Holger Krekel and others
6 """Discover and run ipdoctests in modules and test files."""
6 """Discover and run ipdoctests in modules and test files."""
7 import builtins
8 import bdb
7 import bdb
8 import builtins
9 import inspect
9 import inspect
10 import os
10 import os
11 import platform
11 import platform
12 import sys
12 import sys
13 import traceback
13 import traceback
14 import types
14 import types
15 import warnings
15 import warnings
16 from contextlib import contextmanager
16 from contextlib import contextmanager
17 from pathlib import Path
17 from pathlib import Path
18 from typing import Any
18 from typing import (
19 from typing import Callable
19 TYPE_CHECKING,
20 from typing import Dict
20 Any,
21 from typing import Generator
21 Callable,
22 from typing import Iterable
22 Dict,
23 from typing import List
23 Generator,
24 from typing import Optional
24 Iterable,
25 from typing import Pattern
25 List,
26 from typing import Sequence
26 Optional,
27 from typing import Tuple
27 Pattern,
28 from typing import Type
28 Sequence,
29 from typing import TYPE_CHECKING
29 Tuple,
30 from typing import Union
30 Type,
31 Union,
32 )
31
33
32 import pytest
34 import pytest
33 from _pytest import outcomes
35 from _pytest import outcomes
34 from _pytest._code.code import ExceptionInfo
36 from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
35 from _pytest._code.code import ReprFileLocation
36 from _pytest._code.code import TerminalRepr
37 from _pytest._io import TerminalWriter
37 from _pytest._io import TerminalWriter
38 from _pytest.compat import safe_getattr
38 from _pytest.compat import safe_getattr
39 from _pytest.config import Config
39 from _pytest.config import Config
40 from _pytest.config.argparsing import Parser
40 from _pytest.config.argparsing import Parser
41 from _pytest.fixtures import FixtureRequest
41 from _pytest.fixtures import FixtureRequest
42 from _pytest.nodes import Collector
42 from _pytest.nodes import Collector
43 from _pytest.outcomes import OutcomeException
43 from _pytest.outcomes import OutcomeException
44 from _pytest.pathlib import fnmatch_ex
44 from _pytest.pathlib import fnmatch_ex, import_path
45 from _pytest.pathlib import import_path
46 from _pytest.python_api import approx
45 from _pytest.python_api import approx
47 from _pytest.warning_types import PytestWarning
46 from _pytest.warning_types import PytestWarning
48
47
49 if TYPE_CHECKING:
48 if TYPE_CHECKING:
50 import doctest
49 import doctest
51
50
51 from .ipdoctest import IPDoctestOutputChecker
52
52 DOCTEST_REPORT_CHOICE_NONE = "none"
53 DOCTEST_REPORT_CHOICE_NONE = "none"
53 DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
54 DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
54 DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
55 DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
55 DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
56 DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
56 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
57 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
57
58
58 DOCTEST_REPORT_CHOICES = (
59 DOCTEST_REPORT_CHOICES = (
59 DOCTEST_REPORT_CHOICE_NONE,
60 DOCTEST_REPORT_CHOICE_NONE,
60 DOCTEST_REPORT_CHOICE_CDIFF,
61 DOCTEST_REPORT_CHOICE_CDIFF,
61 DOCTEST_REPORT_CHOICE_NDIFF,
62 DOCTEST_REPORT_CHOICE_NDIFF,
62 DOCTEST_REPORT_CHOICE_UDIFF,
63 DOCTEST_REPORT_CHOICE_UDIFF,
63 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
64 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
64 )
65 )
65
66
66 # Lazy definition of runner class
67 # Lazy definition of runner class
67 RUNNER_CLASS = None
68 RUNNER_CLASS = None
68 # Lazy definition of output checker class
69 # Lazy definition of output checker class
69 CHECKER_CLASS: Optional[Type["IPDoctestOutputChecker"]] = None
70 CHECKER_CLASS: Optional[Type["IPDoctestOutputChecker"]] = None
70
71
71
72
72 def pytest_addoption(parser: Parser) -> None:
73 def pytest_addoption(parser: Parser) -> None:
73 parser.addini(
74 parser.addini(
74 "ipdoctest_optionflags",
75 "ipdoctest_optionflags",
75 "option flags for ipdoctests",
76 "option flags for ipdoctests",
76 type="args",
77 type="args",
77 default=["ELLIPSIS"],
78 default=["ELLIPSIS"],
78 )
79 )
79 parser.addini(
80 parser.addini(
80 "ipdoctest_encoding", "encoding used for ipdoctest files", default="utf-8"
81 "ipdoctest_encoding", "encoding used for ipdoctest files", default="utf-8"
81 )
82 )
82 group = parser.getgroup("collect")
83 group = parser.getgroup("collect")
83 group.addoption(
84 group.addoption(
84 "--ipdoctest-modules",
85 "--ipdoctest-modules",
85 action="store_true",
86 action="store_true",
86 default=False,
87 default=False,
87 help="run ipdoctests in all .py modules",
88 help="run ipdoctests in all .py modules",
88 dest="ipdoctestmodules",
89 dest="ipdoctestmodules",
89 )
90 )
90 group.addoption(
91 group.addoption(
91 "--ipdoctest-report",
92 "--ipdoctest-report",
92 type=str.lower,
93 type=str.lower,
93 default="udiff",
94 default="udiff",
94 help="choose another output format for diffs on ipdoctest failure",
95 help="choose another output format for diffs on ipdoctest failure",
95 choices=DOCTEST_REPORT_CHOICES,
96 choices=DOCTEST_REPORT_CHOICES,
96 dest="ipdoctestreport",
97 dest="ipdoctestreport",
97 )
98 )
98 group.addoption(
99 group.addoption(
99 "--ipdoctest-glob",
100 "--ipdoctest-glob",
100 action="append",
101 action="append",
101 default=[],
102 default=[],
102 metavar="pat",
103 metavar="pat",
103 help="ipdoctests file matching pattern, default: test*.txt",
104 help="ipdoctests file matching pattern, default: test*.txt",
104 dest="ipdoctestglob",
105 dest="ipdoctestglob",
105 )
106 )
106 group.addoption(
107 group.addoption(
107 "--ipdoctest-ignore-import-errors",
108 "--ipdoctest-ignore-import-errors",
108 action="store_true",
109 action="store_true",
109 default=False,
110 default=False,
110 help="ignore ipdoctest ImportErrors",
111 help="ignore ipdoctest ImportErrors",
111 dest="ipdoctest_ignore_import_errors",
112 dest="ipdoctest_ignore_import_errors",
112 )
113 )
113 group.addoption(
114 group.addoption(
114 "--ipdoctest-continue-on-failure",
115 "--ipdoctest-continue-on-failure",
115 action="store_true",
116 action="store_true",
116 default=False,
117 default=False,
117 help="for a given ipdoctest, continue to run after the first failure",
118 help="for a given ipdoctest, continue to run after the first failure",
118 dest="ipdoctest_continue_on_failure",
119 dest="ipdoctest_continue_on_failure",
119 )
120 )
120
121
121
122
122 def pytest_unconfigure() -> None:
123 def pytest_unconfigure() -> None:
123 global RUNNER_CLASS
124 global RUNNER_CLASS
124
125
125 RUNNER_CLASS = None
126 RUNNER_CLASS = None
126
127
127
128
128 def pytest_collect_file(
129 def pytest_collect_file(
129 file_path: Path,
130 file_path: Path,
130 parent: Collector,
131 parent: Collector,
131 ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
132 ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
132 config = parent.config
133 config = parent.config
133 if file_path.suffix == ".py":
134 if file_path.suffix == ".py":
134 if config.option.ipdoctestmodules and not any(
135 if config.option.ipdoctestmodules and not any(
135 (_is_setup_py(file_path), _is_main_py(file_path))
136 (_is_setup_py(file_path), _is_main_py(file_path))
136 ):
137 ):
137 mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path)
138 mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path)
138 return mod
139 return mod
139 elif _is_ipdoctest(config, file_path, parent):
140 elif _is_ipdoctest(config, file_path, parent):
140 txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path)
141 txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path)
141 return txt
142 return txt
142 return None
143 return None
143
144
144
145
145 if int(pytest.__version__.split(".")[0]) < 7:
146 if int(pytest.__version__.split(".")[0]) < 7:
146 _collect_file = pytest_collect_file
147 _collect_file = pytest_collect_file
147
148
148 def pytest_collect_file(
149 def pytest_collect_file(
149 path,
150 path,
150 parent: Collector,
151 parent: Collector,
151 ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
152 ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
152 return _collect_file(Path(path), parent)
153 return _collect_file(Path(path), parent)
153
154
154 _import_path = import_path
155 _import_path = import_path
155
156
156 def import_path(path, root):
157 def import_path(path, root):
157 import py.path
158 import py.path
158
159
159 return _import_path(py.path.local(path))
160 return _import_path(py.path.local(path))
160
161
161
162
162 def _is_setup_py(path: Path) -> bool:
163 def _is_setup_py(path: Path) -> bool:
163 if path.name != "setup.py":
164 if path.name != "setup.py":
164 return False
165 return False
165 contents = path.read_bytes()
166 contents = path.read_bytes()
166 return b"setuptools" in contents or b"distutils" in contents
167 return b"setuptools" in contents or b"distutils" in contents
167
168
168
169
169 def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool:
170 def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool:
170 if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
171 if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
171 return True
172 return True
172 globs = config.getoption("ipdoctestglob") or ["test*.txt"]
173 globs = config.getoption("ipdoctestglob") or ["test*.txt"]
173 return any(fnmatch_ex(glob, path) for glob in globs)
174 return any(fnmatch_ex(glob, path) for glob in globs)
174
175
175
176
176 def _is_main_py(path: Path) -> bool:
177 def _is_main_py(path: Path) -> bool:
177 return path.name == "__main__.py"
178 return path.name == "__main__.py"
178
179
179
180
180 class ReprFailDoctest(TerminalRepr):
181 class ReprFailDoctest(TerminalRepr):
181 def __init__(
182 def __init__(
182 self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
183 self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
183 ) -> None:
184 ) -> None:
184 self.reprlocation_lines = reprlocation_lines
185 self.reprlocation_lines = reprlocation_lines
185
186
186 def toterminal(self, tw: TerminalWriter) -> None:
187 def toterminal(self, tw: TerminalWriter) -> None:
187 for reprlocation, lines in self.reprlocation_lines:
188 for reprlocation, lines in self.reprlocation_lines:
188 for line in lines:
189 for line in lines:
189 tw.line(line)
190 tw.line(line)
190 reprlocation.toterminal(tw)
191 reprlocation.toterminal(tw)
191
192
192
193
193 class MultipleDoctestFailures(Exception):
194 class MultipleDoctestFailures(Exception):
194 def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
195 def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
195 super().__init__()
196 super().__init__()
196 self.failures = failures
197 self.failures = failures
197
198
198
199
199 def _init_runner_class() -> Type["IPDocTestRunner"]:
200 def _init_runner_class() -> Type["IPDocTestRunner"]:
200 import doctest
201 import doctest
201 from .ipdoctest import IPDocTestRunner
202 from .ipdoctest import IPDocTestRunner
202
203
203 class PytestDoctestRunner(IPDocTestRunner):
204 class PytestDoctestRunner(IPDocTestRunner):
204 """Runner to collect failures.
205 """Runner to collect failures.
205
206
206 Note that the out variable in this case is a list instead of a
207 Note that the out variable in this case is a list instead of a
207 stdout-like object.
208 stdout-like object.
208 """
209 """
209
210
210 def __init__(
211 def __init__(
211 self,
212 self,
212 checker: Optional["IPDoctestOutputChecker"] = None,
213 checker: Optional["IPDoctestOutputChecker"] = None,
213 verbose: Optional[bool] = None,
214 verbose: Optional[bool] = None,
214 optionflags: int = 0,
215 optionflags: int = 0,
215 continue_on_failure: bool = True,
216 continue_on_failure: bool = True,
216 ) -> None:
217 ) -> None:
217 super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
218 super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
218 self.continue_on_failure = continue_on_failure
219 self.continue_on_failure = continue_on_failure
219
220
220 def report_failure(
221 def report_failure(
221 self,
222 self,
222 out,
223 out,
223 test: "doctest.DocTest",
224 test: "doctest.DocTest",
224 example: "doctest.Example",
225 example: "doctest.Example",
225 got: str,
226 got: str,
226 ) -> None:
227 ) -> None:
227 failure = doctest.DocTestFailure(test, example, got)
228 failure = doctest.DocTestFailure(test, example, got)
228 if self.continue_on_failure:
229 if self.continue_on_failure:
229 out.append(failure)
230 out.append(failure)
230 else:
231 else:
231 raise failure
232 raise failure
232
233
233 def report_unexpected_exception(
234 def report_unexpected_exception(
234 self,
235 self,
235 out,
236 out,
236 test: "doctest.DocTest",
237 test: "doctest.DocTest",
237 example: "doctest.Example",
238 example: "doctest.Example",
238 exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
239 exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
239 ) -> None:
240 ) -> None:
240 if isinstance(exc_info[1], OutcomeException):
241 if isinstance(exc_info[1], OutcomeException):
241 raise exc_info[1]
242 raise exc_info[1]
242 if isinstance(exc_info[1], bdb.BdbQuit):
243 if isinstance(exc_info[1], bdb.BdbQuit):
243 outcomes.exit("Quitting debugger")
244 outcomes.exit("Quitting debugger")
244 failure = doctest.UnexpectedException(test, example, exc_info)
245 failure = doctest.UnexpectedException(test, example, exc_info)
245 if self.continue_on_failure:
246 if self.continue_on_failure:
246 out.append(failure)
247 out.append(failure)
247 else:
248 else:
248 raise failure
249 raise failure
249
250
250 return PytestDoctestRunner
251 return PytestDoctestRunner
251
252
252
253
253 def _get_runner(
254 def _get_runner(
254 checker: Optional["IPDoctestOutputChecker"] = None,
255 checker: Optional["IPDoctestOutputChecker"] = None,
255 verbose: Optional[bool] = None,
256 verbose: Optional[bool] = None,
256 optionflags: int = 0,
257 optionflags: int = 0,
257 continue_on_failure: bool = True,
258 continue_on_failure: bool = True,
258 ) -> "IPDocTestRunner":
259 ) -> "IPDocTestRunner":
259 # We need this in order to do a lazy import on doctest
260 # We need this in order to do a lazy import on doctest
260 global RUNNER_CLASS
261 global RUNNER_CLASS
261 if RUNNER_CLASS is None:
262 if RUNNER_CLASS is None:
262 RUNNER_CLASS = _init_runner_class()
263 RUNNER_CLASS = _init_runner_class()
263 # Type ignored because the continue_on_failure argument is only defined on
264 # Type ignored because the continue_on_failure argument is only defined on
264 # PytestDoctestRunner, which is lazily defined so can't be used as a type.
265 # PytestDoctestRunner, which is lazily defined so can't be used as a type.
265 return RUNNER_CLASS( # type: ignore
266 return RUNNER_CLASS( # type: ignore
266 checker=checker,
267 checker=checker,
267 verbose=verbose,
268 verbose=verbose,
268 optionflags=optionflags,
269 optionflags=optionflags,
269 continue_on_failure=continue_on_failure,
270 continue_on_failure=continue_on_failure,
270 )
271 )
271
272
272
273
273 class IPDoctestItem(pytest.Item):
274 class IPDoctestItem(pytest.Item):
275 _user_ns_orig: Dict[str, Any]
276
274 def __init__(
277 def __init__(
275 self,
278 self,
276 name: str,
279 name: str,
277 parent: "Union[IPDoctestTextfile, IPDoctestModule]",
280 parent: "Union[IPDoctestTextfile, IPDoctestModule]",
278 runner: Optional["IPDocTestRunner"] = None,
281 runner: Optional["IPDocTestRunner"] = None,
279 dtest: Optional["doctest.DocTest"] = None,
282 dtest: Optional["doctest.DocTest"] = None,
280 ) -> None:
283 ) -> None:
281 super().__init__(name, parent)
284 super().__init__(name, parent)
282 self.runner = runner
285 self.runner = runner
283 self.dtest = dtest
286 self.dtest = dtest
284 self.obj = None
287 self.obj = None
285 self.fixture_request: Optional[FixtureRequest] = None
288 self.fixture_request: Optional[FixtureRequest] = None
289 self._user_ns_orig = {}
286
290
287 @classmethod
291 @classmethod
288 def from_parent( # type: ignore
292 def from_parent( # type: ignore
289 cls,
293 cls,
290 parent: "Union[IPDoctestTextfile, IPDoctestModule]",
294 parent: "Union[IPDoctestTextfile, IPDoctestModule]",
291 *,
295 *,
292 name: str,
296 name: str,
293 runner: "IPDocTestRunner",
297 runner: "IPDocTestRunner",
294 dtest: "doctest.DocTest",
298 dtest: "doctest.DocTest",
295 ):
299 ):
296 # incompatible signature due to imposed limits on subclass
300 # incompatible signature due to imposed limits on subclass
297 """The public named constructor."""
301 """The public named constructor."""
298 return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
302 return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
299
303
300 def setup(self) -> None:
304 def setup(self) -> None:
301 if self.dtest is not None:
305 if self.dtest is not None:
302 self.fixture_request = _setup_fixtures(self)
306 self.fixture_request = _setup_fixtures(self)
303 globs = dict(getfixture=self.fixture_request.getfixturevalue)
307 globs = dict(getfixture=self.fixture_request.getfixturevalue)
304 for name, value in self.fixture_request.getfixturevalue(
308 for name, value in self.fixture_request.getfixturevalue(
305 "ipdoctest_namespace"
309 "ipdoctest_namespace"
306 ).items():
310 ).items():
307 globs[name] = value
311 globs[name] = value
308 self.dtest.globs.update(globs)
312 self.dtest.globs.update(globs)
309
313
310 from .ipdoctest import IPExample
314 from .ipdoctest import IPExample
311
315
312 if isinstance(self.dtest.examples[0], IPExample):
316 if isinstance(self.dtest.examples[0], IPExample):
313 # for IPython examples *only*, we swap the globals with the ipython
317 # for IPython examples *only*, we swap the globals with the ipython
314 # namespace, after updating it with the globals (which doctest
318 # namespace, after updating it with the globals (which doctest
315 # fills with the necessary info from the module being tested).
319 # fills with the necessary info from the module being tested).
316 self._user_ns_orig = {}
320 self._user_ns_orig = {}
317 self._user_ns_orig.update(_ip.user_ns)
321 self._user_ns_orig.update(_ip.user_ns)
318 _ip.user_ns.update(self.dtest.globs)
322 _ip.user_ns.update(self.dtest.globs)
319 # We must remove the _ key in the namespace, so that Python's
323 # We must remove the _ key in the namespace, so that Python's
320 # doctest code sets it naturally
324 # doctest code sets it naturally
321 _ip.user_ns.pop("_", None)
325 _ip.user_ns.pop("_", None)
322 _ip.user_ns["__builtins__"] = builtins
326 _ip.user_ns["__builtins__"] = builtins
323 self.dtest.globs = _ip.user_ns
327 self.dtest.globs = _ip.user_ns
324
328
325 def teardown(self) -> None:
329 def teardown(self) -> None:
326 from .ipdoctest import IPExample
330 from .ipdoctest import IPExample
327
331
328 # Undo the test.globs reassignment we made
332 # Undo the test.globs reassignment we made
329 if isinstance(self.dtest.examples[0], IPExample):
333 if isinstance(self.dtest.examples[0], IPExample):
330 self.dtest.globs = {}
334 self.dtest.globs = {}
331 _ip.user_ns.clear()
335 _ip.user_ns.clear()
332 _ip.user_ns.update(self._user_ns_orig)
336 _ip.user_ns.update(self._user_ns_orig)
333 del self._user_ns_orig
337 del self._user_ns_orig
334
338
335 self.dtest.globs.clear()
339 self.dtest.globs.clear()
336
340
337 def runtest(self) -> None:
341 def runtest(self) -> None:
338 assert self.dtest is not None
342 assert self.dtest is not None
339 assert self.runner is not None
343 assert self.runner is not None
340 _check_all_skipped(self.dtest)
344 _check_all_skipped(self.dtest)
341 self._disable_output_capturing_for_darwin()
345 self._disable_output_capturing_for_darwin()
342 failures: List["doctest.DocTestFailure"] = []
346 failures: List["doctest.DocTestFailure"] = []
343
347
344 # exec(compile(..., "single", ...), ...) puts result in builtins._
348 # exec(compile(..., "single", ...), ...) puts result in builtins._
345 had_underscore_value = hasattr(builtins, "_")
349 had_underscore_value = hasattr(builtins, "_")
346 underscore_original_value = getattr(builtins, "_", None)
350 underscore_original_value = getattr(builtins, "_", None)
347
351
348 # Save our current directory and switch out to the one where the
352 # Save our current directory and switch out to the one where the
349 # test was originally created, in case another doctest did a
353 # test was originally created, in case another doctest did a
350 # directory change. We'll restore this in the finally clause.
354 # directory change. We'll restore this in the finally clause.
351 curdir = os.getcwd()
355 curdir = os.getcwd()
352 os.chdir(self.fspath.dirname)
356 os.chdir(self.fspath.dirname)
353 try:
357 try:
354 # Type ignored because we change the type of `out` from what
358 # Type ignored because we change the type of `out` from what
355 # ipdoctest expects.
359 # ipdoctest expects.
356 self.runner.run(self.dtest, out=failures, clear_globs=False) # type: ignore[arg-type]
360 self.runner.run(self.dtest, out=failures, clear_globs=False) # type: ignore[arg-type]
357 finally:
361 finally:
358 os.chdir(curdir)
362 os.chdir(curdir)
359 if had_underscore_value:
363 if had_underscore_value:
360 setattr(builtins, "_", underscore_original_value)
364 setattr(builtins, "_", underscore_original_value)
361 elif hasattr(builtins, "_"):
365 elif hasattr(builtins, "_"):
362 delattr(builtins, "_")
366 delattr(builtins, "_")
363
367
364 if failures:
368 if failures:
365 raise MultipleDoctestFailures(failures)
369 raise MultipleDoctestFailures(failures)
366
370
367 def _disable_output_capturing_for_darwin(self) -> None:
371 def _disable_output_capturing_for_darwin(self) -> None:
368 """Disable output capturing. Otherwise, stdout is lost to ipdoctest (pytest#985)."""
372 """Disable output capturing. Otherwise, stdout is lost to ipdoctest (pytest#985)."""
369 if platform.system() != "Darwin":
373 if platform.system() != "Darwin":
370 return
374 return
371 capman = self.config.pluginmanager.getplugin("capturemanager")
375 capman = self.config.pluginmanager.getplugin("capturemanager")
372 if capman:
376 if capman:
373 capman.suspend_global_capture(in_=True)
377 capman.suspend_global_capture(in_=True)
374 out, err = capman.read_global_capture()
378 out, err = capman.read_global_capture()
375 sys.stdout.write(out)
379 sys.stdout.write(out)
376 sys.stderr.write(err)
380 sys.stderr.write(err)
377
381
378 # TODO: Type ignored -- breaks Liskov Substitution.
382 # TODO: Type ignored -- breaks Liskov Substitution.
379 def repr_failure( # type: ignore[override]
383 def repr_failure( # type: ignore[override]
380 self,
384 self,
381 excinfo: ExceptionInfo[BaseException],
385 excinfo: ExceptionInfo[BaseException],
382 ) -> Union[str, TerminalRepr]:
386 ) -> Union[str, TerminalRepr]:
383 import doctest
387 import doctest
384
388
385 failures: Optional[
389 failures: Optional[
386 Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
390 Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
387 ] = None
391 ] = None
388 if isinstance(
392 if isinstance(
389 excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
393 excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
390 ):
394 ):
391 failures = [excinfo.value]
395 failures = [excinfo.value]
392 elif isinstance(excinfo.value, MultipleDoctestFailures):
396 elif isinstance(excinfo.value, MultipleDoctestFailures):
393 failures = excinfo.value.failures
397 failures = excinfo.value.failures
394
398
395 if failures is None:
399 if failures is None:
396 return super().repr_failure(excinfo)
400 return super().repr_failure(excinfo)
397
401
398 reprlocation_lines = []
402 reprlocation_lines = []
399 for failure in failures:
403 for failure in failures:
400 example = failure.example
404 example = failure.example
401 test = failure.test
405 test = failure.test
402 filename = test.filename
406 filename = test.filename
403 if test.lineno is None:
407 if test.lineno is None:
404 lineno = None
408 lineno = None
405 else:
409 else:
406 lineno = test.lineno + example.lineno + 1
410 lineno = test.lineno + example.lineno + 1
407 message = type(failure).__name__
411 message = type(failure).__name__
408 # TODO: ReprFileLocation doesn't expect a None lineno.
412 # TODO: ReprFileLocation doesn't expect a None lineno.
409 reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
413 reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
410 checker = _get_checker()
414 checker = _get_checker()
411 report_choice = _get_report_choice(self.config.getoption("ipdoctestreport"))
415 report_choice = _get_report_choice(self.config.getoption("ipdoctestreport"))
412 if lineno is not None:
416 if lineno is not None:
413 assert failure.test.docstring is not None
417 assert failure.test.docstring is not None
414 lines = failure.test.docstring.splitlines(False)
418 lines = failure.test.docstring.splitlines(False)
415 # add line numbers to the left of the error message
419 # add line numbers to the left of the error message
416 assert test.lineno is not None
420 assert test.lineno is not None
417 lines = [
421 lines = [
418 "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
422 "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
419 ]
423 ]
420 # trim docstring error lines to 10
424 # trim docstring error lines to 10
421 lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
425 lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
422 else:
426 else:
423 lines = [
427 lines = [
424 "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
428 "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
425 ]
429 ]
426 indent = ">>>"
430 indent = ">>>"
427 for line in example.source.splitlines():
431 for line in example.source.splitlines():
428 lines.append(f"??? {indent} {line}")
432 lines.append(f"??? {indent} {line}")
429 indent = "..."
433 indent = "..."
430 if isinstance(failure, doctest.DocTestFailure):
434 if isinstance(failure, doctest.DocTestFailure):
431 lines += checker.output_difference(
435 lines += checker.output_difference(
432 example, failure.got, report_choice
436 example, failure.got, report_choice
433 ).split("\n")
437 ).split("\n")
434 else:
438 else:
435 inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
439 inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
436 lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
440 lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
437 lines += [
441 lines += [
438 x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
442 x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
439 ]
443 ]
440 reprlocation_lines.append((reprlocation, lines))
444 reprlocation_lines.append((reprlocation, lines))
441 return ReprFailDoctest(reprlocation_lines)
445 return ReprFailDoctest(reprlocation_lines)
442
446
443 def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
447 def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
444 assert self.dtest is not None
448 assert self.dtest is not None
445 return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name
449 return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name
446
450
447 if int(pytest.__version__.split(".")[0]) < 7:
451 if int(pytest.__version__.split(".")[0]) < 7:
448
452
449 @property
453 @property
450 def path(self) -> Path:
454 def path(self) -> Path:
451 return Path(self.fspath)
455 return Path(self.fspath)
452
456
453
457
454 def _get_flag_lookup() -> Dict[str, int]:
458 def _get_flag_lookup() -> Dict[str, int]:
455 import doctest
459 import doctest
456
460
457 return dict(
461 return dict(
458 DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
462 DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
459 DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
463 DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
460 NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
464 NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
461 ELLIPSIS=doctest.ELLIPSIS,
465 ELLIPSIS=doctest.ELLIPSIS,
462 IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
466 IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
463 COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
467 COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
464 ALLOW_UNICODE=_get_allow_unicode_flag(),
468 ALLOW_UNICODE=_get_allow_unicode_flag(),
465 ALLOW_BYTES=_get_allow_bytes_flag(),
469 ALLOW_BYTES=_get_allow_bytes_flag(),
466 NUMBER=_get_number_flag(),
470 NUMBER=_get_number_flag(),
467 )
471 )
468
472
469
473
470 def get_optionflags(parent):
474 def get_optionflags(parent):
471 optionflags_str = parent.config.getini("ipdoctest_optionflags")
475 optionflags_str = parent.config.getini("ipdoctest_optionflags")
472 flag_lookup_table = _get_flag_lookup()
476 flag_lookup_table = _get_flag_lookup()
473 flag_acc = 0
477 flag_acc = 0
474 for flag in optionflags_str:
478 for flag in optionflags_str:
475 flag_acc |= flag_lookup_table[flag]
479 flag_acc |= flag_lookup_table[flag]
476 return flag_acc
480 return flag_acc
477
481
478
482
479 def _get_continue_on_failure(config):
483 def _get_continue_on_failure(config):
480 continue_on_failure = config.getvalue("ipdoctest_continue_on_failure")
484 continue_on_failure = config.getvalue("ipdoctest_continue_on_failure")
481 if continue_on_failure:
485 if continue_on_failure:
482 # We need to turn off this if we use pdb since we should stop at
486 # We need to turn off this if we use pdb since we should stop at
483 # the first failure.
487 # the first failure.
484 if config.getvalue("usepdb"):
488 if config.getvalue("usepdb"):
485 continue_on_failure = False
489 continue_on_failure = False
486 return continue_on_failure
490 return continue_on_failure
487
491
488
492
489 class IPDoctestTextfile(pytest.Module):
493 class IPDoctestTextfile(pytest.Module):
490 obj = None
494 obj = None
491
495
492 def collect(self) -> Iterable[IPDoctestItem]:
496 def collect(self) -> Iterable[IPDoctestItem]:
493 import doctest
497 import doctest
494 from .ipdoctest import IPDocTestParser
498 from .ipdoctest import IPDocTestParser
495
499
496 # Inspired by doctest.testfile; ideally we would use it directly,
500 # Inspired by doctest.testfile; ideally we would use it directly,
497 # but it doesn't support passing a custom checker.
501 # but it doesn't support passing a custom checker.
498 encoding = self.config.getini("ipdoctest_encoding")
502 encoding = self.config.getini("ipdoctest_encoding")
499 text = self.path.read_text(encoding)
503 text = self.path.read_text(encoding)
500 filename = str(self.path)
504 filename = str(self.path)
501 name = self.path.name
505 name = self.path.name
502 globs = {"__name__": "__main__"}
506 globs = {"__name__": "__main__"}
503
507
504 optionflags = get_optionflags(self)
508 optionflags = get_optionflags(self)
505
509
506 runner = _get_runner(
510 runner = _get_runner(
507 verbose=False,
511 verbose=False,
508 optionflags=optionflags,
512 optionflags=optionflags,
509 checker=_get_checker(),
513 checker=_get_checker(),
510 continue_on_failure=_get_continue_on_failure(self.config),
514 continue_on_failure=_get_continue_on_failure(self.config),
511 )
515 )
512
516
513 parser = IPDocTestParser()
517 parser = IPDocTestParser()
514 test = parser.get_doctest(text, globs, name, filename, 0)
518 test = parser.get_doctest(text, globs, name, filename, 0)
515 if test.examples:
519 if test.examples:
516 yield IPDoctestItem.from_parent(
520 yield IPDoctestItem.from_parent(
517 self, name=test.name, runner=runner, dtest=test
521 self, name=test.name, runner=runner, dtest=test
518 )
522 )
519
523
520 if int(pytest.__version__.split(".")[0]) < 7:
524 if int(pytest.__version__.split(".")[0]) < 7:
521
525
522 @property
526 @property
523 def path(self) -> Path:
527 def path(self) -> Path:
524 return Path(self.fspath)
528 return Path(self.fspath)
525
529
526 @classmethod
530 @classmethod
527 def from_parent(
531 def from_parent(
528 cls,
532 cls,
529 parent,
533 parent,
530 *,
534 *,
531 fspath=None,
535 fspath=None,
532 path: Optional[Path] = None,
536 path: Optional[Path] = None,
533 **kw,
537 **kw,
534 ):
538 ):
535 if path is not None:
539 if path is not None:
536 import py.path
540 import py.path
537
541
538 fspath = py.path.local(path)
542 fspath = py.path.local(path)
539 return super().from_parent(parent=parent, fspath=fspath, **kw)
543 return super().from_parent(parent=parent, fspath=fspath, **kw)
540
544
541
545
542 def _check_all_skipped(test: "doctest.DocTest") -> None:
546 def _check_all_skipped(test: "doctest.DocTest") -> None:
543 """Raise pytest.skip() if all examples in the given DocTest have the SKIP
547 """Raise pytest.skip() if all examples in the given DocTest have the SKIP
544 option set."""
548 option set."""
545 import doctest
549 import doctest
546
550
547 all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
551 all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
548 if all_skipped:
552 if all_skipped:
549 pytest.skip("all docstests skipped by +SKIP option")
553 pytest.skip("all docstests skipped by +SKIP option")
550
554
551
555
552 def _is_mocked(obj: object) -> bool:
556 def _is_mocked(obj: object) -> bool:
553 """Return if an object is possibly a mock object by checking the
557 """Return if an object is possibly a mock object by checking the
554 existence of a highly improbable attribute."""
558 existence of a highly improbable attribute."""
555 return (
559 return (
556 safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
560 safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
557 is not None
561 is not None
558 )
562 )
559
563
560
564
561 @contextmanager
565 @contextmanager
562 def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
566 def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
563 """Context manager which replaces ``inspect.unwrap`` with a version
567 """Context manager which replaces ``inspect.unwrap`` with a version
564 that's aware of mock objects and doesn't recurse into them."""
568 that's aware of mock objects and doesn't recurse into them."""
565 real_unwrap = inspect.unwrap
569 real_unwrap = inspect.unwrap
566
570
567 def _mock_aware_unwrap(
571 def _mock_aware_unwrap(
568 func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
572 func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
569 ) -> Any:
573 ) -> Any:
570 try:
574 try:
571 if stop is None or stop is _is_mocked:
575 if stop is None or stop is _is_mocked:
572 return real_unwrap(func, stop=_is_mocked)
576 return real_unwrap(func, stop=_is_mocked)
573 _stop = stop
577 _stop = stop
574 return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
578 return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
575 except Exception as e:
579 except Exception as e:
576 warnings.warn(
580 warnings.warn(
577 "Got %r when unwrapping %r. This is usually caused "
581 "Got %r when unwrapping %r. This is usually caused "
578 "by a violation of Python's object protocol; see e.g. "
582 "by a violation of Python's object protocol; see e.g. "
579 "https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
583 "https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
580 PytestWarning,
584 PytestWarning,
581 )
585 )
582 raise
586 raise
583
587
584 inspect.unwrap = _mock_aware_unwrap
588 inspect.unwrap = _mock_aware_unwrap
585 try:
589 try:
586 yield
590 yield
587 finally:
591 finally:
588 inspect.unwrap = real_unwrap
592 inspect.unwrap = real_unwrap
589
593
590
594
591 class IPDoctestModule(pytest.Module):
595 class IPDoctestModule(pytest.Module):
592 def collect(self) -> Iterable[IPDoctestItem]:
596 def collect(self) -> Iterable[IPDoctestItem]:
593 import doctest
597 import doctest
594 from .ipdoctest import DocTestFinder, IPDocTestParser
598 from .ipdoctest import DocTestFinder, IPDocTestParser
595
599
596 class MockAwareDocTestFinder(DocTestFinder):
600 class MockAwareDocTestFinder(DocTestFinder):
597 """A hackish ipdoctest finder that overrides stdlib internals to fix a stdlib bug.
601 """A hackish ipdoctest finder that overrides stdlib internals to fix a stdlib bug.
598
602
599 https://github.com/pytest-dev/pytest/issues/3456
603 https://github.com/pytest-dev/pytest/issues/3456
600 https://bugs.python.org/issue25532
604 https://bugs.python.org/issue25532
601 """
605 """
602
606
603 def _find_lineno(self, obj, source_lines):
607 def _find_lineno(self, obj, source_lines):
604 """Doctest code does not take into account `@property`, this
608 """Doctest code does not take into account `@property`, this
605 is a hackish way to fix it. https://bugs.python.org/issue17446
609 is a hackish way to fix it. https://bugs.python.org/issue17446
606
610
607 Wrapped Doctests will need to be unwrapped so the correct
611 Wrapped Doctests will need to be unwrapped so the correct
608 line number is returned. This will be reported upstream. #8796
612 line number is returned. This will be reported upstream. #8796
609 """
613 """
610 if isinstance(obj, property):
614 if isinstance(obj, property):
611 obj = getattr(obj, "fget", obj)
615 obj = getattr(obj, "fget", obj)
612
616
613 if hasattr(obj, "__wrapped__"):
617 if hasattr(obj, "__wrapped__"):
614 # Get the main obj in case of it being wrapped
618 # Get the main obj in case of it being wrapped
615 obj = inspect.unwrap(obj)
619 obj = inspect.unwrap(obj)
616
620
617 # Type ignored because this is a private function.
621 # Type ignored because this is a private function.
618 return super()._find_lineno( # type:ignore[misc]
622 return super()._find_lineno( # type:ignore[misc]
619 obj,
623 obj,
620 source_lines,
624 source_lines,
621 )
625 )
622
626
623 def _find(
627 def _find(
624 self, tests, obj, name, module, source_lines, globs, seen
628 self, tests, obj, name, module, source_lines, globs, seen
625 ) -> None:
629 ) -> None:
626 if _is_mocked(obj):
630 if _is_mocked(obj):
627 return
631 return
628 with _patch_unwrap_mock_aware():
632 with _patch_unwrap_mock_aware():
629 # Type ignored because this is a private function.
633 # Type ignored because this is a private function.
630 super()._find( # type:ignore[misc]
634 super()._find( # type:ignore[misc]
631 tests, obj, name, module, source_lines, globs, seen
635 tests, obj, name, module, source_lines, globs, seen
632 )
636 )
633
637
634 if self.path.name == "conftest.py":
638 if self.path.name == "conftest.py":
635 if int(pytest.__version__.split(".")[0]) < 7:
639 if int(pytest.__version__.split(".")[0]) < 7:
636 module = self.config.pluginmanager._importconftest(
640 module = self.config.pluginmanager._importconftest(
637 self.path,
641 self.path,
638 self.config.getoption("importmode"),
642 self.config.getoption("importmode"),
639 )
643 )
640 else:
644 else:
641 module = self.config.pluginmanager._importconftest(
645 module = self.config.pluginmanager._importconftest(
642 self.path,
646 self.path,
643 self.config.getoption("importmode"),
647 self.config.getoption("importmode"),
644 rootpath=self.config.rootpath,
648 rootpath=self.config.rootpath,
645 )
649 )
646 else:
650 else:
647 try:
651 try:
648 module = import_path(self.path, root=self.config.rootpath)
652 module = import_path(self.path, root=self.config.rootpath)
649 except ImportError:
653 except ImportError:
650 if self.config.getvalue("ipdoctest_ignore_import_errors"):
654 if self.config.getvalue("ipdoctest_ignore_import_errors"):
651 pytest.skip("unable to import module %r" % self.path)
655 pytest.skip("unable to import module %r" % self.path)
652 else:
656 else:
653 raise
657 raise
654 # Uses internal doctest module parsing mechanism.
658 # Uses internal doctest module parsing mechanism.
655 finder = MockAwareDocTestFinder(parser=IPDocTestParser())
659 finder = MockAwareDocTestFinder(parser=IPDocTestParser())
656 optionflags = get_optionflags(self)
660 optionflags = get_optionflags(self)
657 runner = _get_runner(
661 runner = _get_runner(
658 verbose=False,
662 verbose=False,
659 optionflags=optionflags,
663 optionflags=optionflags,
660 checker=_get_checker(),
664 checker=_get_checker(),
661 continue_on_failure=_get_continue_on_failure(self.config),
665 continue_on_failure=_get_continue_on_failure(self.config),
662 )
666 )
663
667
664 for test in finder.find(module, module.__name__):
668 for test in finder.find(module, module.__name__):
665 if test.examples: # skip empty ipdoctests
669 if test.examples: # skip empty ipdoctests
666 yield IPDoctestItem.from_parent(
670 yield IPDoctestItem.from_parent(
667 self, name=test.name, runner=runner, dtest=test
671 self, name=test.name, runner=runner, dtest=test
668 )
672 )
669
673
670 if int(pytest.__version__.split(".")[0]) < 7:
674 if int(pytest.__version__.split(".")[0]) < 7:
671
675
672 @property
676 @property
673 def path(self) -> Path:
677 def path(self) -> Path:
674 return Path(self.fspath)
678 return Path(self.fspath)
675
679
676 @classmethod
680 @classmethod
677 def from_parent(
681 def from_parent(
678 cls,
682 cls,
679 parent,
683 parent,
680 *,
684 *,
681 fspath=None,
685 fspath=None,
682 path: Optional[Path] = None,
686 path: Optional[Path] = None,
683 **kw,
687 **kw,
684 ):
688 ):
685 if path is not None:
689 if path is not None:
686 import py.path
690 import py.path
687
691
688 fspath = py.path.local(path)
692 fspath = py.path.local(path)
689 return super().from_parent(parent=parent, fspath=fspath, **kw)
693 return super().from_parent(parent=parent, fspath=fspath, **kw)
690
694
691
695
692 def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest:
696 def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest:
693 """Used by IPDoctestTextfile and IPDoctestItem to setup fixture information."""
697 """Used by IPDoctestTextfile and IPDoctestItem to setup fixture information."""
694
698
695 def func() -> None:
699 def func() -> None:
696 pass
700 pass
697
701
698 doctest_item.funcargs = {} # type: ignore[attr-defined]
702 doctest_item.funcargs = {} # type: ignore[attr-defined]
699 fm = doctest_item.session._fixturemanager
703 fm = doctest_item.session._fixturemanager
700 doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
704 doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
701 node=doctest_item, func=func, cls=None, funcargs=False
705 node=doctest_item, func=func, cls=None, funcargs=False
702 )
706 )
703 fixture_request = FixtureRequest(doctest_item, _ispytest=True)
707 fixture_request = FixtureRequest(doctest_item, _ispytest=True)
704 fixture_request._fillfixtures()
708 fixture_request._fillfixtures()
705 return fixture_request
709 return fixture_request
706
710
707
711
708 def _init_checker_class() -> Type["IPDoctestOutputChecker"]:
712 def _init_checker_class() -> Type["IPDoctestOutputChecker"]:
709 import doctest
713 import doctest
710 import re
714 import re
711 from .ipdoctest import IPDoctestOutputChecker
715 from .ipdoctest import IPDoctestOutputChecker
712
716
713 class LiteralsOutputChecker(IPDoctestOutputChecker):
717 class LiteralsOutputChecker(IPDoctestOutputChecker):
714 # Based on doctest_nose_plugin.py from the nltk project
718 # Based on doctest_nose_plugin.py from the nltk project
715 # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
719 # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
716 # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
720 # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
717
721
718 _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
722 _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
719 _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
723 _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
720 _number_re = re.compile(
724 _number_re = re.compile(
721 r"""
725 r"""
722 (?P<number>
726 (?P<number>
723 (?P<mantissa>
727 (?P<mantissa>
724 (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
728 (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
725 |
729 |
726 (?P<integer2> [+-]?\d+)\.
730 (?P<integer2> [+-]?\d+)\.
727 )
731 )
728 (?:
732 (?:
729 [Ee]
733 [Ee]
730 (?P<exponent1> [+-]?\d+)
734 (?P<exponent1> [+-]?\d+)
731 )?
735 )?
732 |
736 |
733 (?P<integer3> [+-]?\d+)
737 (?P<integer3> [+-]?\d+)
734 (?:
738 (?:
735 [Ee]
739 [Ee]
736 (?P<exponent2> [+-]?\d+)
740 (?P<exponent2> [+-]?\d+)
737 )
741 )
738 )
742 )
739 """,
743 """,
740 re.VERBOSE,
744 re.VERBOSE,
741 )
745 )
742
746
743 def check_output(self, want: str, got: str, optionflags: int) -> bool:
747 def check_output(self, want: str, got: str, optionflags: int) -> bool:
744 if super().check_output(want, got, optionflags):
748 if super().check_output(want, got, optionflags):
745 return True
749 return True
746
750
747 allow_unicode = optionflags & _get_allow_unicode_flag()
751 allow_unicode = optionflags & _get_allow_unicode_flag()
748 allow_bytes = optionflags & _get_allow_bytes_flag()
752 allow_bytes = optionflags & _get_allow_bytes_flag()
749 allow_number = optionflags & _get_number_flag()
753 allow_number = optionflags & _get_number_flag()
750
754
751 if not allow_unicode and not allow_bytes and not allow_number:
755 if not allow_unicode and not allow_bytes and not allow_number:
752 return False
756 return False
753
757
754 def remove_prefixes(regex: Pattern[str], txt: str) -> str:
758 def remove_prefixes(regex: Pattern[str], txt: str) -> str:
755 return re.sub(regex, r"\1\2", txt)
759 return re.sub(regex, r"\1\2", txt)
756
760
757 if allow_unicode:
761 if allow_unicode:
758 want = remove_prefixes(self._unicode_literal_re, want)
762 want = remove_prefixes(self._unicode_literal_re, want)
759 got = remove_prefixes(self._unicode_literal_re, got)
763 got = remove_prefixes(self._unicode_literal_re, got)
760
764
761 if allow_bytes:
765 if allow_bytes:
762 want = remove_prefixes(self._bytes_literal_re, want)
766 want = remove_prefixes(self._bytes_literal_re, want)
763 got = remove_prefixes(self._bytes_literal_re, got)
767 got = remove_prefixes(self._bytes_literal_re, got)
764
768
765 if allow_number:
769 if allow_number:
766 got = self._remove_unwanted_precision(want, got)
770 got = self._remove_unwanted_precision(want, got)
767
771
768 return super().check_output(want, got, optionflags)
772 return super().check_output(want, got, optionflags)
769
773
770 def _remove_unwanted_precision(self, want: str, got: str) -> str:
774 def _remove_unwanted_precision(self, want: str, got: str) -> str:
771 wants = list(self._number_re.finditer(want))
775 wants = list(self._number_re.finditer(want))
772 gots = list(self._number_re.finditer(got))
776 gots = list(self._number_re.finditer(got))
773 if len(wants) != len(gots):
777 if len(wants) != len(gots):
774 return got
778 return got
775 offset = 0
779 offset = 0
776 for w, g in zip(wants, gots):
780 for w, g in zip(wants, gots):
777 fraction: Optional[str] = w.group("fraction")
781 fraction: Optional[str] = w.group("fraction")
778 exponent: Optional[str] = w.group("exponent1")
782 exponent: Optional[str] = w.group("exponent1")
779 if exponent is None:
783 if exponent is None:
780 exponent = w.group("exponent2")
784 exponent = w.group("exponent2")
781 precision = 0 if fraction is None else len(fraction)
785 precision = 0 if fraction is None else len(fraction)
782 if exponent is not None:
786 if exponent is not None:
783 precision -= int(exponent)
787 precision -= int(exponent)
784 if float(w.group()) == approx(float(g.group()), abs=10**-precision):
788 if float(w.group()) == approx(float(g.group()), abs=10**-precision):
785 # They're close enough. Replace the text we actually
789 # They're close enough. Replace the text we actually
786 # got with the text we want, so that it will match when we
790 # got with the text we want, so that it will match when we
787 # check the string literally.
791 # check the string literally.
788 got = (
792 got = (
789 got[: g.start() + offset] + w.group() + got[g.end() + offset :]
793 got[: g.start() + offset] + w.group() + got[g.end() + offset :]
790 )
794 )
791 offset += w.end() - w.start() - (g.end() - g.start())
795 offset += w.end() - w.start() - (g.end() - g.start())
792 return got
796 return got
793
797
794 return LiteralsOutputChecker
798 return LiteralsOutputChecker
795
799
796
800
797 def _get_checker() -> "IPDoctestOutputChecker":
801 def _get_checker() -> "IPDoctestOutputChecker":
798 """Return a IPDoctestOutputChecker subclass that supports some
802 """Return a IPDoctestOutputChecker subclass that supports some
799 additional options:
803 additional options:
800
804
801 * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
805 * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
802 prefixes (respectively) in string literals. Useful when the same
806 prefixes (respectively) in string literals. Useful when the same
803 ipdoctest should run in Python 2 and Python 3.
807 ipdoctest should run in Python 2 and Python 3.
804
808
805 * NUMBER to ignore floating-point differences smaller than the
809 * NUMBER to ignore floating-point differences smaller than the
806 precision of the literal number in the ipdoctest.
810 precision of the literal number in the ipdoctest.
807
811
808 An inner class is used to avoid importing "ipdoctest" at the module
812 An inner class is used to avoid importing "ipdoctest" at the module
809 level.
813 level.
810 """
814 """
811 global CHECKER_CLASS
815 global CHECKER_CLASS
812 if CHECKER_CLASS is None:
816 if CHECKER_CLASS is None:
813 CHECKER_CLASS = _init_checker_class()
817 CHECKER_CLASS = _init_checker_class()
814 return CHECKER_CLASS()
818 return CHECKER_CLASS()
815
819
816
820
817 def _get_allow_unicode_flag() -> int:
821 def _get_allow_unicode_flag() -> int:
818 """Register and return the ALLOW_UNICODE flag."""
822 """Register and return the ALLOW_UNICODE flag."""
819 import doctest
823 import doctest
820
824
821 return doctest.register_optionflag("ALLOW_UNICODE")
825 return doctest.register_optionflag("ALLOW_UNICODE")
822
826
823
827
824 def _get_allow_bytes_flag() -> int:
828 def _get_allow_bytes_flag() -> int:
825 """Register and return the ALLOW_BYTES flag."""
829 """Register and return the ALLOW_BYTES flag."""
826 import doctest
830 import doctest
827
831
828 return doctest.register_optionflag("ALLOW_BYTES")
832 return doctest.register_optionflag("ALLOW_BYTES")
829
833
830
834
831 def _get_number_flag() -> int:
835 def _get_number_flag() -> int:
832 """Register and return the NUMBER flag."""
836 """Register and return the NUMBER flag."""
833 import doctest
837 import doctest
834
838
835 return doctest.register_optionflag("NUMBER")
839 return doctest.register_optionflag("NUMBER")
836
840
837
841
838 def _get_report_choice(key: str) -> int:
842 def _get_report_choice(key: str) -> int:
839 """Return the actual `ipdoctest` module flag value.
843 """Return the actual `ipdoctest` module flag value.
840
844
841 We want to do it as late as possible to avoid importing `ipdoctest` and all
845 We want to do it as late as possible to avoid importing `ipdoctest` and all
842 its dependencies when parsing options, as it adds overhead and breaks tests.
846 its dependencies when parsing options, as it adds overhead and breaks tests.
843 """
847 """
844 import doctest
848 import doctest
845
849
846 return {
850 return {
847 DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
851 DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
848 DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
852 DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
849 DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
853 DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
850 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
854 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
851 DOCTEST_REPORT_CHOICE_NONE: 0,
855 DOCTEST_REPORT_CHOICE_NONE: 0,
852 }[key]
856 }[key]
853
857
854
858
855 @pytest.fixture(scope="session")
859 @pytest.fixture(scope="session")
856 def ipdoctest_namespace() -> Dict[str, Any]:
860 def ipdoctest_namespace() -> Dict[str, Any]:
857 """Fixture that returns a :py:class:`dict` that will be injected into the
861 """Fixture that returns a :py:class:`dict` that will be injected into the
858 namespace of ipdoctests."""
862 namespace of ipdoctests."""
859 return dict()
863 return dict()
@@ -1,166 +1,168 b''
1 """Tests for the decorators we've created for IPython.
1 """Tests for the decorators we've created for IPython.
2 """
2 """
3
3
4 # Module imports
4 # Module imports
5 # Std lib
5 # Std lib
6 import inspect
6 import inspect
7 import sys
7 import sys
8
8
9 # Our own
9 # Our own
10 from IPython.testing import decorators as dec
10 from IPython.testing import decorators as dec
11 from IPython.testing.skipdoctest import skip_doctest
11 from IPython.testing.skipdoctest import skip_doctest
12 from IPython.utils.text import dedent
12
13
13 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
14 # Utilities
15 # Utilities
15
16
16 # Note: copied from OInspect, kept here so the testing stuff doesn't create
17 # Note: copied from OInspect, kept here so the testing stuff doesn't create
17 # circular dependencies and is easier to reuse.
18 # circular dependencies and is easier to reuse.
18 def getargspec(obj):
19 def getargspec(obj):
19 """Get the names and default values of a function's arguments.
20 """Get the names and default values of a function's arguments.
20
21
21 A tuple of four things is returned: (args, varargs, varkw, defaults).
22 A tuple of four things is returned: (args, varargs, varkw, defaults).
22 'args' is a list of the argument names (it may contain nested lists).
23 'args' is a list of the argument names (it may contain nested lists).
23 'varargs' and 'varkw' are the names of the * and ** arguments or None.
24 'varargs' and 'varkw' are the names of the * and ** arguments or None.
24 'defaults' is an n-tuple of the default values of the last n arguments.
25 'defaults' is an n-tuple of the default values of the last n arguments.
25
26
26 Modified version of inspect.getargspec from the Python Standard
27 Modified version of inspect.getargspec from the Python Standard
27 Library."""
28 Library."""
28
29
29 if inspect.isfunction(obj):
30 if inspect.isfunction(obj):
30 func_obj = obj
31 func_obj = obj
31 elif inspect.ismethod(obj):
32 elif inspect.ismethod(obj):
32 func_obj = obj.__func__
33 func_obj = obj.__func__
33 else:
34 else:
34 raise TypeError('arg is not a Python function')
35 raise TypeError('arg is not a Python function')
35 args, varargs, varkw = inspect.getargs(func_obj.__code__)
36 args, varargs, varkw = inspect.getargs(func_obj.__code__)
36 return args, varargs, varkw, func_obj.__defaults__
37 return args, varargs, varkw, func_obj.__defaults__
37
38
38 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
39 # Testing functions
40 # Testing functions
40
41
41 @dec.as_unittest
42 @dec.as_unittest
42 def trivial():
43 def trivial():
43 """A trivial test"""
44 """A trivial test"""
44 pass
45 pass
45
46
46
47
47 @dec.skip()
48 @dec.skip()
48 def test_deliberately_broken():
49 def test_deliberately_broken():
49 """A deliberately broken test - we want to skip this one."""
50 """A deliberately broken test - we want to skip this one."""
50 1/0
51 1/0
51
52
52 @dec.skip('Testing the skip decorator')
53 @dec.skip('Testing the skip decorator')
53 def test_deliberately_broken2():
54 def test_deliberately_broken2():
54 """Another deliberately broken test - we want to skip this one."""
55 """Another deliberately broken test - we want to skip this one."""
55 1/0
56 1/0
56
57
57
58
58 # Verify that we can correctly skip the doctest for a function at will, but
59 # Verify that we can correctly skip the doctest for a function at will, but
59 # that the docstring itself is NOT destroyed by the decorator.
60 # that the docstring itself is NOT destroyed by the decorator.
60 @skip_doctest
61 @skip_doctest
61 def doctest_bad(x,y=1,**k):
62 def doctest_bad(x,y=1,**k):
62 """A function whose doctest we need to skip.
63 """A function whose doctest we need to skip.
63
64
64 >>> 1+1
65 >>> 1+1
65 3
66 3
66 """
67 """
67 print('x:',x)
68 print('x:',x)
68 print('y:',y)
69 print('y:',y)
69 print('k:',k)
70 print('k:',k)
70
71
71
72
72 def call_doctest_bad():
73 def call_doctest_bad():
73 """Check that we can still call the decorated functions.
74 """Check that we can still call the decorated functions.
74
75
75 >>> doctest_bad(3,y=4)
76 >>> doctest_bad(3,y=4)
76 x: 3
77 x: 3
77 y: 4
78 y: 4
78 k: {}
79 k: {}
79 """
80 """
80 pass
81 pass
81
82
82
83
83 def test_skip_dt_decorator():
84 def test_skip_dt_decorator():
84 """Doctest-skipping decorator should preserve the docstring.
85 """Doctest-skipping decorator should preserve the docstring.
85 """
86 """
86 # Careful: 'check' must be a *verbatim* copy of the doctest_bad docstring!
87 # Careful: 'check' must be a *verbatim* copy of the doctest_bad docstring!
87 check = """A function whose doctest we need to skip.
88 check = """A function whose doctest we need to skip.
88
89
89 >>> 1+1
90 >>> 1+1
90 3
91 3
91 """
92 """
93
92 # Fetch the docstring from doctest_bad after decoration.
94 # Fetch the docstring from doctest_bad after decoration.
93 val = doctest_bad.__doc__
95 val = doctest_bad.__doc__
94
96
95 assert check == val, "doctest_bad docstrings don't match"
97 assert dedent(check) == dedent(val), "doctest_bad docstrings don't match"
96
98
97
99
98 # Doctest skipping should work for class methods too
100 # Doctest skipping should work for class methods too
99 class FooClass(object):
101 class FooClass(object):
100 """FooClass
102 """FooClass
101
103
102 Example:
104 Example:
103
105
104 >>> 1+1
106 >>> 1+1
105 2
107 2
106 """
108 """
107
109
108 @skip_doctest
110 @skip_doctest
109 def __init__(self,x):
111 def __init__(self,x):
110 """Make a FooClass.
112 """Make a FooClass.
111
113
112 Example:
114 Example:
113
115
114 >>> f = FooClass(3)
116 >>> f = FooClass(3)
115 junk
117 junk
116 """
118 """
117 print('Making a FooClass.')
119 print('Making a FooClass.')
118 self.x = x
120 self.x = x
119
121
120 @skip_doctest
122 @skip_doctest
121 def bar(self,y):
123 def bar(self,y):
122 """Example:
124 """Example:
123
125
124 >>> ff = FooClass(3)
126 >>> ff = FooClass(3)
125 >>> ff.bar(0)
127 >>> ff.bar(0)
126 boom!
128 boom!
127 >>> 1/0
129 >>> 1/0
128 bam!
130 bam!
129 """
131 """
130 return 1/y
132 return 1/y
131
133
132 def baz(self,y):
134 def baz(self,y):
133 """Example:
135 """Example:
134
136
135 >>> ff2 = FooClass(3)
137 >>> ff2 = FooClass(3)
136 Making a FooClass.
138 Making a FooClass.
137 >>> ff2.baz(3)
139 >>> ff2.baz(3)
138 True
140 True
139 """
141 """
140 return self.x==y
142 return self.x==y
141
143
142
144
143 def test_skip_dt_decorator2():
145 def test_skip_dt_decorator2():
144 """Doctest-skipping decorator should preserve function signature.
146 """Doctest-skipping decorator should preserve function signature.
145 """
147 """
146 # Hardcoded correct answer
148 # Hardcoded correct answer
147 dtargs = (['x', 'y'], None, 'k', (1,))
149 dtargs = (['x', 'y'], None, 'k', (1,))
148 # Introspect out the value
150 # Introspect out the value
149 dtargsr = getargspec(doctest_bad)
151 dtargsr = getargspec(doctest_bad)
150 assert dtargsr==dtargs, \
152 assert dtargsr==dtargs, \
151 "Incorrectly reconstructed args for doctest_bad: %s" % (dtargsr,)
153 "Incorrectly reconstructed args for doctest_bad: %s" % (dtargsr,)
152
154
153
155
154 @dec.skip_linux
156 @dec.skip_linux
155 def test_linux():
157 def test_linux():
156 assert sys.platform.startswith("linux") is False, "This test can't run under linux"
158 assert sys.platform.startswith("linux") is False, "This test can't run under linux"
157
159
158
160
159 @dec.skip_win32
161 @dec.skip_win32
160 def test_win32():
162 def test_win32():
161 assert sys.platform != "win32", "This test can't run under windows"
163 assert sys.platform != "win32", "This test can't run under windows"
162
164
163
165
164 @dec.skip_osx
166 @dec.skip_osx
165 def test_osx():
167 def test_osx():
166 assert sys.platform != "darwin", "This test can't run under osx"
168 assert sys.platform != "darwin", "This test can't run under osx"
@@ -1,116 +1,115 b''
1 [metadata]
1 [metadata]
2 name = ipython
2 name = ipython
3 version = attr: IPython.core.release.__version__
3 version = attr: IPython.core.release.__version__
4 url = https://ipython.org
4 url = https://ipython.org
5 description = IPython: Productive Interactive Computing
5 description = IPython: Productive Interactive Computing
6 long_description_content_type = text/x-rst
6 long_description_content_type = text/x-rst
7 long_description = file: long_description.rst
7 long_description = file: long_description.rst
8 license_file = LICENSE
8 license_file = LICENSE
9 project_urls =
9 project_urls =
10 Documentation = https://ipython.readthedocs.io/
10 Documentation = https://ipython.readthedocs.io/
11 Funding = https://numfocus.org/
11 Funding = https://numfocus.org/
12 Source = https://github.com/ipython/ipython
12 Source = https://github.com/ipython/ipython
13 Tracker = https://github.com/ipython/ipython/issues
13 Tracker = https://github.com/ipython/ipython/issues
14 keywords = Interactive, Interpreter, Shell, Embedding
14 keywords = Interactive, Interpreter, Shell, Embedding
15 platforms = Linux, Mac OSX, Windows
15 platforms = Linux, Mac OSX, Windows
16 classifiers =
16 classifiers =
17 Framework :: IPython
17 Framework :: IPython
18 Framework :: Jupyter
18 Framework :: Jupyter
19 Intended Audience :: Developers
19 Intended Audience :: Developers
20 Intended Audience :: Science/Research
20 Intended Audience :: Science/Research
21 License :: OSI Approved :: BSD License
21 License :: OSI Approved :: BSD License
22 Programming Language :: Python
22 Programming Language :: Python
23 Programming Language :: Python :: 3
23 Programming Language :: Python :: 3
24 Programming Language :: Python :: 3 :: Only
24 Programming Language :: Python :: 3 :: Only
25 Topic :: System :: Shells
25 Topic :: System :: Shells
26
26
27 [options]
27 [options]
28 packages = find:
28 packages = find:
29 python_requires = >=3.10
29 python_requires = >=3.10
30 zip_safe = False
30 zip_safe = False
31 install_requires =
31 install_requires =
32 colorama; sys_platform == "win32"
32 colorama; sys_platform == "win32"
33 decorator
33 decorator
34 exceptiongroup; python_version<'3.11'
34 exceptiongroup; python_version<'3.11'
35 jedi>=0.16
35 jedi>=0.16
36 matplotlib-inline
36 matplotlib-inline
37 pexpect>4.3; sys_platform != "win32"
37 pexpect>4.3; sys_platform != "win32"
38 prompt_toolkit>=3.0.41,<3.1.0
38 prompt_toolkit>=3.0.41,<3.1.0
39 pygments>=2.4.0
39 pygments>=2.4.0
40 stack_data
40 stack_data
41 traitlets>=5
41 traitlets>=5
42 typing_extensions ; python_version<'3.10'
42 typing_extensions ; python_version<'3.10'
43
43
44 [options.extras_require]
44 [options.extras_require]
45 black =
45 black =
46 black
46 black
47 doc =
47 doc =
48 ipykernel
48 ipykernel
49 setuptools>=18.5
49 setuptools>=18.5
50 sphinx>=1.3
50 sphinx>=1.3
51 sphinx-rtd-theme
51 sphinx-rtd-theme
52 docrepr
52 docrepr
53 matplotlib
53 matplotlib
54 stack_data
54 stack_data
55 pytest
56 typing_extensions
55 typing_extensions
57 exceptiongroup
56 exceptiongroup
58 %(test)s
57 %(test)s
59 kernel =
58 kernel =
60 ipykernel
59 ipykernel
61 nbconvert =
60 nbconvert =
62 nbconvert
61 nbconvert
63 nbformat =
62 nbformat =
64 nbformat
63 nbformat
65 notebook =
64 notebook =
66 ipywidgets
65 ipywidgets
67 notebook
66 notebook
68 parallel =
67 parallel =
69 ipyparallel
68 ipyparallel
70 qtconsole =
69 qtconsole =
71 qtconsole
70 qtconsole
72 terminal =
71 terminal =
73 test =
72 test =
74 pytest
73 pytest<8
75 pytest-asyncio<0.22
74 pytest-asyncio<0.22
76 testpath
75 testpath
77 pickleshare
76 pickleshare
78 test_extra =
77 test_extra =
79 %(test)s
78 %(test)s
80 curio
79 curio
81 matplotlib!=3.2.0
80 matplotlib!=3.2.0
82 nbformat
81 nbformat
83 numpy>=1.23
82 numpy>=1.23
84 pandas
83 pandas
85 trio
84 trio
86 all =
85 all =
87 %(black)s
86 %(black)s
88 %(doc)s
87 %(doc)s
89 %(kernel)s
88 %(kernel)s
90 %(nbconvert)s
89 %(nbconvert)s
91 %(nbformat)s
90 %(nbformat)s
92 %(notebook)s
91 %(notebook)s
93 %(parallel)s
92 %(parallel)s
94 %(qtconsole)s
93 %(qtconsole)s
95 %(terminal)s
94 %(terminal)s
96 %(test_extra)s
95 %(test_extra)s
97 %(test)s
96 %(test)s
98
97
99 [options.packages.find]
98 [options.packages.find]
100 exclude =
99 exclude =
101 setupext
100 setupext
102
101
103 [options.package_data]
102 [options.package_data]
104 IPython = py.typed
103 IPython = py.typed
105 IPython.core = profile/README*
104 IPython.core = profile/README*
106 IPython.core.tests = *.png, *.jpg, daft_extension/*.py
105 IPython.core.tests = *.png, *.jpg, daft_extension/*.py
107 IPython.lib.tests = *.wav
106 IPython.lib.tests = *.wav
108 IPython.testing.plugin = *.txt
107 IPython.testing.plugin = *.txt
109
108
110 [velin]
109 [velin]
111 ignore_patterns =
110 ignore_patterns =
112 IPython/core/tests
111 IPython/core/tests
113 IPython/testing
112 IPython/testing
114
113
115 [tool.black]
114 [tool.black]
116 exclude = 'timing\.py'
115 exclude = 'timing\.py'
General Comments 0
You need to be logged in to leave comments. Login now