diff --git a/.circleci/config.yml b/.circleci/config.yml index 36241cb6..436d47df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -118,15 +118,17 @@ jobs: name: run tests command: | . venv/bin/activate + ls + $PYTHON_EXE -m pip freeze $PYTHON_EXE run_tests.py # Upload to codecov.io (requires a CODECOV_TOKEN environ or github+circleci integration) - codecov - - store_artifacts: - path: test-reports - destination: test-reports - - store_artifacts: - path: .coverage - destination: .coverage + #codecov + #- store_artifacts: + # path: test-reports + # destination: test-reports + #- store_artifacts: + # path: .coverage + # destination: .coverage .test_full_template: &test_full_template @@ -160,15 +162,17 @@ jobs: name: run tests command: | . venv/bin/activate + ls + $PYTHON_EXE -m pip freeze $PYTHON_EXE run_tests.py # Upload to codecov.io (requires a CODECOV_TOKEN environ or github+circleci integration) - codecov - - store_artifacts: - path: test-reports - destination: test-reports - - store_artifacts: - path: .coverage - destination: .coverage + #codecov + #- store_artifacts: + # path: test-reports + # destination: test-reports + #- store_artifacts: + # path: .coverage + # destination: .coverage ################################### diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 66ec35f3..eba0abc0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ # Based on ~/code/xcookie/xcookie/rc/tests.yml.in # Now based on ~/code/xcookie/xcookie/builders/github_actions.py -name: PurePy Build and Test +name: PurePyCI on: push: @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4.3.0 + - name: Set up Python 3.11 for linting + uses: actions/setup-python@v4.5.0 with: - python-version: 3.8 + python-version: '3.11' - name: Install dependencies run: |- python -m pip install --upgrade pip @@ -34,15 +34,15 @@ jobs: mypy --install-types --non-interactive ./src/xdoctest mypy ./src/xdoctest build_and_test_sdist: - name: Test sdist Python 3.8 + name: Build sdist runs-on: ubuntu-latest steps: - name: Checkout source uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4.3.0 + - name: Set up Python 3.11 + uses: actions/setup-python@v4.5.0 with: - python-version: 3.8 + python-version: '3.11' - name: Upgrade pip run: |- python -m pip install --upgrade pip @@ -57,7 +57,7 @@ jobs: - name: Install sdist run: |- ls -al ./wheelhouse - pip install wheelhouse/xdoctest*.tar.gz -v + pip install --prefer-binary wheelhouse/xdoctest*.tar.gz -v - name: Test minimal loose sdist run: |- pwd @@ -92,156 +92,232 @@ jobs: with: name: wheels path: ./wheelhouse/*.tar.gz - build_and_test_purepy_wheels: + build_purepy_wheels: name: ${{ matrix.python-version }} on ${{ matrix.os }}, arch=${{ matrix.arch }} with ${{ matrix.install-extras }} runs-on: ${{ matrix.os }} strategy: matrix: + os: + - ubuntu-latest + python-version: + - '3.11' arch: - auto + steps: + - name: Checkout source + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + if: runner.os == 'Linux' && matrix.arch != 'auto' + with: + platforms: all + - name: Setup Python + uses: actions/setup-python@v4.5.0 + with: + python-version: ${{ matrix.python-version }} + - name: Build pure wheel + shell: bash + run: |- + python -m pip install pip -U + python -m pip install setuptools>=0.8 build + python -m build --wheel --outdir wheelhouse + - name: Show built files + shell: bash + run: ls -la wheelhouse + - uses: actions/upload-artifact@v3 + name: Upload wheels artifact + with: + name: wheels + path: ./wheelhouse/xdoctest*.whl + test_purepy_wheels: + name: ${{ matrix.python-version }} on ${{ matrix.os }}, arch=${{ matrix.arch }} with ${{ matrix.install-extras }} + runs-on: ${{ matrix.os }} + needs: + - build_purepy_wheels + strategy: + matrix: include: - python-version: '3.6' - os: ubuntu-latest + os: ubuntu-20.04 install-extras: tests-strict,runtime-strict + arch: auto - python-version: '3.6' os: macOS-latest install-extras: tests-strict,runtime-strict + arch: auto - python-version: '3.6' os: windows-latest install-extras: tests-strict,runtime-strict + arch: auto - python-version: '3.11' os: ubuntu-latest install-extras: tests-strict,runtime-strict,optional-strict + arch: auto - python-version: '3.11' os: macOS-latest install-extras: tests-strict,runtime-strict,optional-strict + arch: auto - python-version: '3.11' os: windows-latest install-extras: tests-strict,runtime-strict,optional-strict + arch: auto - python-version: '3.11' os: macOS-latest install-extras: tests + arch: auto - python-version: '3.11' os: windows-latest install-extras: tests + arch: auto - python-version: '3.6' - os: ubuntu-latest + os: ubuntu-20.04 install-extras: tests,optional + arch: auto - python-version: '3.7' os: ubuntu-latest install-extras: tests,optional + arch: auto - python-version: '3.8' os: ubuntu-latest install-extras: tests,optional + arch: auto - python-version: '3.9' os: ubuntu-latest install-extras: tests,optional + arch: auto - python-version: '3.10' os: ubuntu-latest install-extras: tests,optional + arch: auto - python-version: '3.11' os: ubuntu-latest install-extras: tests,optional + arch: auto - python-version: '3.6' os: macOS-latest install-extras: tests,optional + arch: auto - python-version: '3.7' os: macOS-latest install-extras: tests,optional + arch: auto - python-version: '3.8' os: macOS-latest install-extras: tests,optional + arch: auto - python-version: '3.9' os: macOS-latest install-extras: tests,optional + arch: auto - python-version: '3.10' os: macOS-latest install-extras: tests,optional + arch: auto - python-version: '3.11' os: macOS-latest install-extras: tests,optional + arch: auto - python-version: '3.6' os: windows-latest install-extras: tests,optional + arch: auto - python-version: '3.7' os: windows-latest install-extras: tests,optional + arch: auto - python-version: '3.8' os: windows-latest install-extras: tests,optional + arch: auto - python-version: '3.9' os: windows-latest install-extras: tests,optional + arch: auto - python-version: '3.10' os: windows-latest install-extras: tests,optional + arch: auto - python-version: '3.11' os: windows-latest install-extras: tests,optional + arch: auto - python-version: pypy-3.7 os: ubuntu-latest install-extras: tests,optional + arch: auto - python-version: pypy-3.7 os: macOS-latest install-extras: tests,optional + arch: auto - python-version: pypy-3.7 os: windows-latest install-extras: tests,optional + arch: auto steps: - name: Checkout source uses: actions/checkout@v3 - name: Enable MSVC 64bit uses: ilammy/msvc-dev-cmd@v1 - if: matrix.os == 'windows-latest' && matrix.cibw_skip == '*-win32' - - name: Enable MSVC 32bit - uses: ilammy/msvc-dev-cmd@v1 - if: matrix.os == 'windows-latest' && matrix.cibw_skip == '*-win_amd64' - with: - arch: x86 + if: matrix.os == 'windows-latest' # && ${{ contains(matrix.cibw_skip, '*-win32') }} + #- name: Enable MSVC 32bit + # uses: ilammy/msvc-dev-cmd@v1 + # if: matrix.os == 'windows-latest' && ${{ contains(matrix.cibw_skip, '*-win_amd64') }} + # with: + # arch: x86 - name: Set up QEMU uses: docker/setup-qemu-action@v2 if: runner.os == 'Linux' && matrix.arch != 'auto' with: platforms: all - name: Setup Python - uses: actions/setup-python@v4.3.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} - - name: Build pure wheel - shell: bash - run: |- - python -m pip install pip -U - python -m pip install setuptools>=0.8 build - python -m build --wheel --outdir wheelhouse + - uses: actions/download-artifact@v3 + name: Download wheels and sdist + with: + name: wheels + path: wheelhouse - name: Test wheel with ${{ matrix.install-extras }} shell: bash env: INSTALL_EXTRAS: ${{ matrix.install-extras }} CI_PYTHON_VERSION: py${{ matrix.python-version }} run: |- - # Find the path to the wheel - WHEEL_FPATH=$(ls wheelhouse/xdoctest*.whl) - # Install the wheel - python -m pip install ${WHEEL_FPATH}[${INSTALL_EXTRAS}] - # Create a sandboxed directory + echo "Finding the path to the wheel" + ls -al wheelhouse + echo "Installing helpers" + pip install tomli pkginfo + MOD_NAME=xdoctest + echo "MOD_NAME=$MOD_NAME" + WHEEL_FPATH=$(python -c "import pathlib; print(str(sorted(pathlib.Path('wheelhouse').glob('$MOD_NAME*.whl'))[-1]).replace(chr(92), chr(47)))") + echo "WHEEL_FPATH=$WHEEL_FPATH" + MOD_VERSION=$(python -c "from pkginfo import Wheel; print(Wheel('$WHEEL_FPATH').version)") + echo "MOD_VERSION=$MOD_VERSION" + echo "Install the wheel (ensureing we are using the version we just built)" + # NOTE: THE VERSION MUST BE NEWER THAN AN EXISTING PYPI VERSION OR THIS MAY FAIL + pip install --prefer-binary "$MOD_NAME[$INSTALL_EXTRAS]==$MOD_VERSION" -f wheelhouse + echo "Install finished. Creating a sandbox directory to test it" WORKSPACE_DNAME="testdir_${CI_PYTHON_VERSION}_${GITHUB_RUN_ID}_${RUNNER_OS}" + echo "WORKSPACE_DNAME=$WORKSPACE_DNAME" mkdir -p $WORKSPACE_DNAME + echo "cd-ing into the workspace" cd $WORKSPACE_DNAME + pwd + ls -al + pip freeze # Get the path to the installed package and run the tests MOD_DPATH=$(python -c "import xdoctest, os; print(os.path.dirname(xdoctest.__file__))") echo "MOD_DPATH = $MOD_DPATH" - python -m pytest -p pytester -p no:doctest --xdoctest --cov-config ../pyproject.toml --cov-report term --cov=xdoctest $MOD_DPATH ../tests + echo "running the pytest command inside the workspace" + python -m pytest -p pytester -p no:doctest --xdoctest --cov-config ../pyproject.toml --cov-report term --cov="$MOD_NAME" "$MOD_DPATH" ../tests + echo "pytest command finished, moving the coverage file to the repo root" + ls -al # Move coverage file to a new name mv .coverage "../.coverage.$WORKSPACE_DNAME" + echo "changing directory back to th repo root" cd .. - - name: Show built files - shell: bash - run: ls -la wheelhouse - - name: Set up Python 3.8 to combine coverage Linux - uses: actions/setup-python@v4.3.0 - if: runner.os == 'Linux' - with: - python-version: 3.8 + ls -al - name: Combine coverage Linux if: runner.os == 'Linux' run: |- @@ -260,18 +336,14 @@ jobs: name: Codecov Upload with: file: ./tests/coverage.xml - - uses: actions/upload-artifact@v3 - name: Upload wheels artifact - with: - name: wheels - path: ./wheelhouse/xdoctest*.whl test_deploy: name: Uploading Test to PyPi runs-on: ubuntu-latest if: github.event_name == 'push' && ! startsWith(github.event.ref, 'refs/tags') && ! startsWith(github.event.ref, 'refs/heads/release') needs: - build_and_test_sdist - - build_and_test_purepy_wheels + - build_purepy_wheels + - test_purepy_wheels steps: - name: Checkout source uses: actions/checkout@v3 @@ -314,7 +386,8 @@ jobs: if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) needs: - build_and_test_sdist - - build_and_test_purepy_wheels + - build_purepy_wheels + - test_purepy_wheels steps: - name: Checkout source uses: actions/checkout@v3 @@ -370,4 +443,4 @@ jobs: # --secret=EROTEMIC_TWINE_USERNAME=$EROTEMIC_TWINE_USERNAME \ # --secret=EROTEMIC_CI_SECRET=$EROTEMIC_CI_SECRET \ # --secret=EROTEMIC_TEST_TWINE_USERNAME=$EROTEMIC_TEST_TWINE_USERNAME \ -# --secret=EROTEMIC_TEST_TWINE_PASSWORD=$EROTEMIC_TEST_TWINE_PASSWORD \ No newline at end of file +# --secret=EROTEMIC_TEST_TWINE_PASSWORD=$EROTEMIC_TEST_TWINE_PASSWORD diff --git a/.readthedocs.yml b/.readthedocs.yml index 8a1cff88..01bd0b26 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,33 +1,18 @@ # .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# +# # See Also: # https://readthedocs.org/dashboard/xdoctest/advanced/ # Required version: 2 - -# Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py - -# Build documentation with MkDocs -#mkdocs: -# configuration: mkdocs.yml - -# Optionally build your docs in additional formats such as PDF and ePub formats: all - -# Optionally set the version of Python and requirements required to build your docs python: version: 3.7 install: - - requirements: requirements/docs.txt - - method: pip - path: . - #extra_requirements: - # - docs - -#conda: -# environment: environment.yml + - requirements: requirements/docs.txt + - method: pip + path: . diff --git a/CHANGELOG.md b/CHANGELOG.md index 1463db45..494e93fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed * Binary tests are now only run on "full" installs to reduce minimal dependencies. +* Support for Python 3.11 +* Minor typing fixes ## Version 1.1.0 - Released 2022-09-05 diff --git a/dev/extension_proposal.py b/dev/extension_proposal.py index e6c3ffc2..76029ba4 100644 --- a/dev/extension_proposal.py +++ b/dev/extension_proposal.py @@ -79,4 +79,85 @@ Again, the problem of distinguishing inputs / outputs persists. + +2023-01-27: New idea: backticks + + +Syntax Idea #1: Use markdown with explicit python tags + +def foobar(): + ''' + Example: + ```python + x = 1 + y = 2 + print(x + y) + ``` + + ```output + 3 + ``` + + ''' + + +Syntax Idea #2: Use a simplified markdown tag that is reminicent of markdown + +def foobar(): + ''' + Example: + `python + x = 1 + y = 2 + print(x + y) + + `output + 3 + + ''' + + +Syntax Idea #3: Lead output with an xdoctest directive + +def foobar(): + ''' + Example: + # xdoctest: example + x = 1 + y = 2 + print(x + y) + + # xdoctest: output + 3 + ''' + + +Shorter prefixes? + +def foobar(): + ''' + Example: + > x = 1 + > y = 2 + > print(x + y) + 3 + ''' + + +First line matters? +def foobar(): + ''' + Example: + + .. code:: python + + x = 1 + y = 2 + print(x + y) + + .. code:: output + + 3 + ''' + """ diff --git a/dev/port_ubelt_utils.py b/dev/port_ubelt_utils.py index d9e72672..e398fada 100644 --- a/dev/port_ubelt_utils.py +++ b/dev/port_ubelt_utils.py @@ -44,7 +44,8 @@ def _autogen_xdoctest_utils(): ''' # -*- coding: utf-8 -*- """ - This file was autogenerated based on code in ubelt + This file was autogenerated based on code in ubelt via + dev/port_ubelt_utils.py in the xdoctest repo """ from __future__ import print_function, division, absolute_import, unicode_literals ''') diff --git a/dev/setup_secrets.sh b/dev/setup_secrets.sh index 7cfdc0da..1ead971d 100644 --- a/dev/setup_secrets.sh +++ b/dev/setup_secrets.sh @@ -393,6 +393,15 @@ export_encrypted_code_signing_keys(){ MAIN_GPG_KEYID=$(gpg --list-keys --keyid-format LONG "$GPG_IDENTIFIER" | head -n 2 | tail -n 1 | awk '{print $1}') GPG_SIGN_SUBKEY=$(gpg --list-keys --with-subkey-fingerprints "$GPG_IDENTIFIER" | grep "\[S\]" -A 1 | tail -n 1 | awk '{print $1}') + # Careful, if you don't have a subkey, requesting it will export more than you want. + # Export the main key instead (its better to have subkeys, but this is a lesser evil) + if [[ "$GPG_SIGN_SUBKEY" == "" ]]; then + # NOTE: if you get here this probably means your subkeys expired (and + # wont even be visible), so we probably should check for that here and + # thrown an error instead of using this hack, which likely wont work + # anyway. + GPG_SIGN_SUBKEY=$(gpg --list-keys --with-subkey-fingerprints "$GPG_IDENTIFIER" | grep "\[C\]" -A 1 | tail -n 1 | awk '{print $1}') + fi echo "MAIN_GPG_KEYID = $MAIN_GPG_KEYID" echo "GPG_SIGN_SUBKEY = $GPG_SIGN_SUBKEY" diff --git a/pyproject.toml b/pyproject.toml index 6fecce5e..6db8489a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ description = "A rewrite of the builtin doctest module" url = "https://github.com/Erotemic/xdoctest" license = "Apache 2" dev_status = "stable" +typed = true classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -29,6 +30,7 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", # Supported Python versions "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.8", @@ -38,6 +40,9 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", ] +[tool.xcookie.setuptools] +keywords = '"xdoctest,doctest,test,docstr,pytest"' + [tool.xcookie.entry_points] # the pytest11 entry point makes the plugin available to pytest pytest11 = [ @@ -50,8 +55,8 @@ console_scripts = [ [tool.pytest.ini_options] -addopts = "-p no:doctest --xdoctest --xdoctest-style=google --ignore-glob=setup.py --ignore-glob=dev" -norecursedirs = ".git ignore build __pycache__ dev _skbuild" +addopts = "-p no:doctest --xdoctest --xdoctest-style=google --ignore-glob=setup.py --ignore-glob=dev --ignore-glob=docs" +norecursedirs = ".git ignore build __pycache__ dev _skbuild docs" filterwarnings = [ "default", "ignore:.*No cfgstr given in Cacher constructor or call.*:Warning", "ignore:.*Define the __nice__ method for.*:Warning", "ignore:.*private pytest class or function.*:Warning",] [tool.coverage.run] diff --git a/requirements.txt b/requirements.txt index 58c4e6d0..bc18a503 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -r requirements/runtime.txt -r requirements/tests.txt --r requirements/tests-binary.txt --r requirements/jupyter.txt --r requirements/colors.txt +-r requirements/optional.txt diff --git a/requirements/tests-binary.txt b/requirements/tests-binary.txt index f88481e1..4200d6c2 100644 --- a/requirements/tests-binary.txt +++ b/requirements/tests-binary.txt @@ -1,5 +1,13 @@ # For testing doctests in binary extension modules -scikit-build>=0.11.1 -cmake>=3.21.2 -ninja>=1.10.2 -pybind11>=2.7.1 + +scikit-build>=0.16.1 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ +scikit-build>=0.11.1 ; python_version < '3.11' # Python <=3.10 + +ninja>=1.11.1 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ +ninja>=1.10.2 ; python_version < '3.11' # Python <=3.10 + +pybind11>=2.10.3 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ +pybind11>=2.7.1 ; python_version < '3.11' # Python <=3.10 + +cmake>=3.25.0 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ +cmake>=3.21.2 ; python_version < '3.11' # Python <=3.10 diff --git a/setup.py b/setup.py index b3e706d4..cfe8ed0a 100755 --- a/setup.py +++ b/setup.py @@ -1,16 +1,18 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from setuptools import setup +# Generated by ~/code/xcookie/xcookie/builders/setup.py +# based on part ~/code/xcookie/xcookie/rc/setup.py.in import sys -from os.path import exists +import re +from os.path import exists, dirname, join from setuptools import find_packages +from setuptools import setup def parse_version(fpath): """ Statically parse the version number from a python file """ - value = static_parse('__version__', fpath) + value = static_parse("__version__", fpath) return value @@ -21,15 +23,15 @@ def static_parse(varname, fpath): import ast if not exists(fpath): - raise ValueError('fpath={!r} does not exist'.format(fpath)) - with open(fpath, 'r') as file_: + raise ValueError("fpath={!r} does not exist".format(fpath)) + with open(fpath, "r") as file_: sourcecode = file_.read() pt = ast.parse(sourcecode) class StaticVisitor(ast.NodeVisitor): def visit_Assign(self, node): for target in node.targets: - if getattr(target, 'id', None) == varname: + if getattr(target, "id", None) == varname: self.static_value = node.value.s visitor = StaticVisitor() @@ -39,7 +41,7 @@ def visit_Assign(self, node): except AttributeError: import warnings - value = 'Unknown {}'.format(varname) + value = "Unknown {}".format(varname) warnings.warn(value) return value @@ -52,18 +54,16 @@ def parse_description(): pandoc --from=markdown --to=rst --output=README.rst README.md python -c "import setup; print(setup.parse_description())" """ - from os.path import dirname, join, exists - - readme_fpath = join(dirname(__file__), 'README.rst') + readme_fpath = join(dirname(__file__), "README.rst") # This breaks on pip install, so check that it exists. if exists(readme_fpath): - with open(readme_fpath, 'r') as f: + with open(readme_fpath, "r") as f: text = f.read() return text - return '' + return "" -def parse_requirements(fname='requirements.txt', versions=False): +def parse_requirements(fname="requirements.txt", versions=False): """ Parse the package dependencies listed in a requirements file but strips specific versioning information. @@ -77,12 +77,9 @@ def parse_requirements(fname='requirements.txt', versions=False): Returns: List[str]: list of requirements items """ - from os.path import exists, dirname, join - import re - require_fpath = fname - def parse_line(line, dpath=''): + def parse_line(line, dpath=""): """ Parse information from a line in a requirements text file @@ -90,147 +87,177 @@ def parse_line(line, dpath=''): line = '-e git+https://a.com/somedep@sometag#egg=SomeDep' """ # Remove inline comments - comment_pos = line.find(' #') + comment_pos = line.find(" #") if comment_pos > -1: line = line[:comment_pos] - if line.startswith('-r '): + if line.startswith("-r "): # Allow specifying requirements in other files - target = join(dpath, line.split(' ')[1]) + target = join(dpath, line.split(" ")[1]) for info in parse_require_file(target): yield info else: # See: https://www.python.org/dev/peps/pep-0508/ - info = {'line': line} - if line.startswith('-e '): - info['package'] = line.split('#egg=')[1] + info = {"line": line} + if line.startswith("-e "): + info["package"] = line.split("#egg=")[1] else: - if ';' in line: - pkgpart, platpart = line.split(';') + if "--find-links" in line: + # setuptools doesnt seem to handle find links + line = line.split("--find-links")[0] + if ";" in line: + pkgpart, platpart = line.split(";") # Handle platform specific dependencies # setuptools.readthedocs.io/en/latest/setuptools.html # #declaring-platform-specific-dependencies plat_deps = platpart.strip() - info['platform_deps'] = plat_deps + info["platform_deps"] = plat_deps else: pkgpart = line platpart = None # Remove versioning from the package - pat = '(' + '|'.join(['>=', '==', '>']) + ')' + pat = "(" + "|".join([">=", "==", ">"]) + ")" parts = re.split(pat, pkgpart, maxsplit=1) parts = [p.strip() for p in parts] - info['package'] = parts[0] + info["package"] = parts[0] if len(parts) > 1: op, rest = parts[1:] version = rest # NOQA - info['version'] = (op, version) + info["version"] = (op, version) yield info def parse_require_file(fpath): dpath = dirname(fpath) - with open(fpath, 'r') as f: + with open(fpath, "r") as f: for line in f.readlines(): line = line.strip() - if line and not line.startswith('#'): + if line and not line.startswith("#"): for info in parse_line(line, dpath=dpath): yield info def gen_packages_items(): if exists(require_fpath): for info in parse_require_file(require_fpath): - parts = [info['package']] - if versions and 'version' in info: - if versions == 'strict': + parts = [info["package"]] + if versions and "version" in info: + if versions == "strict": # In strict mode, we pin to the minimum version - if info['version']: + if info["version"]: # Only replace the first >= instance - verstr = ''.join(info['version']).replace('>=', '==', 1) + verstr = "".join(info["version"]).replace(">=", "==", 1) parts.append(verstr) else: - parts.extend(info['version']) - if not sys.version.startswith('3.4'): + parts.extend(info["version"]) + if not sys.version.startswith("3.4"): # apparently package_deps are broken in 3.4 - plat_deps = info.get('platform_deps') + plat_deps = info.get("platform_deps") if plat_deps is not None: - parts.append(';' + plat_deps) - item = ''.join(parts) + parts.append(";" + plat_deps) + item = "".join(parts) yield item packages = list(gen_packages_items()) return packages -NAME = 'xdoctest' -VERSION = parse_version('src/xdoctest/__init__.py') +# # Maybe use in the future? But has private deps +# def parse_requirements_alt(fpath='requirements.txt', versions='loose'): +# """ +# Args: +# versions (str): can be +# False or "free" - remove all constraints +# True or "loose" - use the greater or equal (>=) in the req file +# strict - replace all greater equal with equals +# """ +# # Note: different versions of pip might have different internals. +# # This may need to be fixed. +# from pip._internal.req import parse_requirements +# from pip._internal.network.session import PipSession +# requirements = [] +# for req in parse_requirements(fpath, session=PipSession()): +# if not versions or versions == 'free': +# req_name = req.requirement.split(' ')[0] +# requirements.append(req_name) +# elif versions == 'loose' or versions is True: +# requirements.append(req.requirement) +# elif versions == 'strict': +# part1, *rest = req.requirement.split(';') +# strict_req = ';'.join([part1.replace('>=', '==')] + rest) +# requirements.append(strict_req) +# else: +# raise KeyError(versions) +# requirements = [r.replace(' ', '') for r in requirements] +# return requirements + -if __name__ == '__main__': +NAME = "xdoctest" +INIT_PATH = "src/xdoctest/__init__.py" +VERSION = parse_version(INIT_PATH) + +if __name__ == "__main__": setupkw = {} - setupkw['install_requires'] = parse_requirements('requirements/runtime.txt') - setupkw['extras_require'] = { - 'all': parse_requirements('requirements.txt'), - 'tests': parse_requirements('requirements/tests.txt'), + + setupkw["install_requires"] = parse_requirements("requirements/runtime.txt") + setupkw["extras_require"] = { + "all": parse_requirements("requirements.txt"), + "tests": parse_requirements("requirements/tests.txt"), + "optional": parse_requirements("requirements/optional.txt"), + "all-strict": parse_requirements("requirements.txt", versions="strict"), + "runtime-strict": parse_requirements( + "requirements/runtime.txt", versions="strict" + ), + "tests-strict": parse_requirements("requirements/tests.txt", versions="strict"), + "optional-strict": parse_requirements( + "requirements/optional.txt", versions="strict" + ), 'tests-binary': parse_requirements('requirements/tests-binary.txt'), - 'optional': parse_requirements('requirements/optional.txt'), - 'all-strict': parse_requirements('requirements.txt', versions='strict'), - 'runtime-strict': parse_requirements('requirements/runtime.txt', versions='strict'), - 'tests-strict': parse_requirements('requirements/tests.txt', versions='strict'), 'tests-binary-strict': parse_requirements('requirements/tests-binary.txt', versions='strict'), - 'optional-strict': parse_requirements('requirements/optional.txt', versions='strict'), 'colors': parse_requirements('requirements/colors.txt'), 'jupyter': parse_requirements('requirements/jupyter.txt'), - } - - setupkw['name'] = NAME - setupkw['version'] = VERSION - setupkw['author'] = 'Jon Crall' - setupkw['author_email'] = 'erotemic@gmail.com' - setupkw['url'] = 'https://github.com/Erotemic/xdoctest' - setupkw['description'] = 'A rewrite of the builtin doctest module' - setupkw['long_description'] = parse_description() - setupkw['long_description_content_type'] = 'text/x-rst' - setupkw['license'] = 'Apache 2' - setupkw['python_requires'] = '>=3.6' - setupkw['classifiers'] = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', + } + + setupkw["name"] = NAME + setupkw["version"] = VERSION + setupkw["author"] = "Jon Crall" + setupkw["author_email"] = "erotemic@gmail.com" + setupkw["url"] = "https://github.com/Erotemic/xdoctest" + setupkw["description"] = "A rewrite of the builtin doctest module" + setupkw["long_description"] = parse_description() + setupkw["long_description_content_type"] = "text/x-rst" + setupkw["license"] = "Apache 2" + setupkw["packages"] = find_packages("./src") + setupkw["python_requires"] = ">=3.6" + setupkw["classifiers"] = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", 'Topic :: Software Development :: Testing', 'Framework :: Pytest', - # This should be interpreted as Apache License v2.0 - 'License :: OSI Approved :: Apache Software License', - # Supported Python versions + "License :: OSI Approved :: Apache Software License", 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.6', + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: CPython', ] - setupkw['keywords'] = 'xdoctest,doctest,test,docstr,pytest' - # https://codefellows.github.io/sea-python-401d4/lectures/python_packaging_1.html - # We use a key of an empty string to indicate that the directory we are - # pointing to should be considered the root. Then the value is src, telling - # setuptools to use that directory as the root of our source. - setupkw['package_dir'] = { - '': 'src', - } - setupkw['packages'] = find_packages('src') - setupkw['package_data'] = { - 'xdoctest': ['py.typed', '*.pyi'], + setupkw["package_data"] = {"xdoctest": ["py.typed", "*.pyi"]} + setupkw["package_dir"] = { + "": "src", } - setupkw['entry_points'] = { - 'console_scripts': [ - 'xdoctest = xdoctest.__main__:main', + setupkw["entry_points"] = { + "console_scripts": [ + "xdoctest = xdoctest.__main__:main", ], - 'pytest11': [ - 'xdoctest = xdoctest.plugin', + "pytest11": [ + "xdoctest = xdoctest.plugin", ], } - if sys.version_info[0] == 3 and sys.version_info[1] <= 4: - setupkw.pop('long_description_content_type', None) + setupkw["keywords"] = "xdoctest,doctest,test,docstr,pytest" setup(**setupkw) diff --git a/src/xdoctest/__init__.py b/src/xdoctest/__init__.py index 326ffdea..fcc72249 100644 --- a/src/xdoctest/__init__.py +++ b/src/xdoctest/__init__.py @@ -314,7 +314,7 @@ def fib(n): mkinit xdoctest --nomods ''' -__version__ = '1.1.0' +__version__ = '1.1.1' # Expose only select submodules diff --git a/src/xdoctest/core.py b/src/xdoctest/core.py index 86bc80c4..739b4354 100644 --- a/src/xdoctest/core.py +++ b/src/xdoctest/core.py @@ -70,25 +70,26 @@ def parse_freeform_docstr_examples(docstr, callname=None, modpath=None, Args: docstr (str): an extracted docstring - callname (str, default=None): + callname (str | None): the name of the callable (e.g. function, class, or method) that this docstring belongs to. - modpath (str | PathLike, default=None): + modpath (str | PathLike | None): original module the docstring is from - lineno (int, default=1): + lineno (int): the line number (starting from 1) of the docstring. i.e. if you were to go to this line number in the source file the starting - quotes of the docstr would be on this line. + quotes of the docstr would be on this line. Defaults to 1. - fpath (str | PathLike, default=None): + fpath (str | PathLike | None): the file that the docstring is from (if the file was not a module, needed for backwards compatibility) - asone (bool, default=True): + asone (bool): if False doctests are broken into multiple examples based on spacing, otherwise they are executed as a single unit. + Defaults to True. Yields: xdoctest.doctest_example.DocTest : doctest object @@ -240,25 +241,25 @@ def parse_google_docstr_examples(docstr, callname=None, modpath=None, lineno=1, Args: docstr (str): an extracted docstring - callname (str, default=None): + callname (str | None): the name of the callable (e.g. function, class, or method) that this docstring belongs to. - modpath (str | PathLike, default=None): + modpath (str | PathLike | None): original module the docstring is from - lineno (int, default=1): + lineno (int): the line number (starting from 1) of the docstring. i.e. if you were to go to this line number in the source file the starting - quotes of the docstr would be on this line. + quotes of the docstr would be on this line. Defaults to 1. - fpath (str | PathLike, default=None): + fpath (str | PathLike | None): the file that the docstring is from (if the file was not a module, needed for backwards compatibility) - eager_parse (bool, default=True): + eager_parse (bool): if True eagerly evaluate the parser inside the google example - blocks + blocks. Defaults to True. Yields: xdoctest.doctest_example.DocTest : doctest object @@ -319,7 +320,7 @@ def parse_auto_docstr_examples(docstr, *args, **kwargs): def parse_docstr_examples(docstr, callname=None, modpath=None, lineno=1, - style='auto', fpath=None, parser_kw={}): + style='auto', fpath=None, parser_kw=None): """ Parses doctests from a docstr and generates example objects. The style influences which tests are found. @@ -327,26 +328,26 @@ def parse_docstr_examples(docstr, callname=None, modpath=None, lineno=1, Args: docstr (str): a previously extracted docstring - callname (str, default=None): + callname (str | None): the name of the callable (e.g. function, class, or method) that this docstring belongs to. - modpath (str | PathLike, default=None): + modpath (str | PathLike | None): original module the docstring is from - lineno (int, default=1): + lineno (int): the line number (starting from 1) of the docstring. i.e. if you were to go to this line number in the source file the starting - quotes of the docstr would be on this line. + quotes of the docstr would be on this line. Defaults to 1. - style (str, default='auto'): expected doctest style, which can - be "google", "freeform", or "auto". + style (str): expected doctest style, which can + be "google", "freeform", or "auto". Defaults to 'auto'. - fpath (str | PathLike, default=None): + fpath (str | PathLike | None): the file that the docstring is from (if the file was not a module, needed for backwards compatibility) - parser_kw (dict, default={}): passed to the parser + parser_kw (dict | None): passed to the parser as keyword args Yields: xdoctest.doctest_example.DocTest : parsed example @@ -391,6 +392,8 @@ def parse_docstr_examples(docstr, callname=None, modpath=None, lineno=1, n_parsed = 0 try: + if parser_kw is None: + parser_kw = {} for example in parser(docstr, callname=callname, modpath=modpath, fpath=fpath, lineno=lineno, **parser_kw): n_parsed += 1 @@ -458,14 +461,15 @@ def package_calldefs(pkg_identifier, exclude=[], ignore_syntax_errors=True, exclude (List[str]): glob-patterns of file names to exclude - ignore_syntax_errors (bool, default=True): + ignore_syntax_errors (bool): if False raise an error when syntax errors occur in a doctest + Defaults to True. - analysis (str, default='auto'): + analysis (str): if 'static', only static analysis is used to parse call definitions. If 'auto', uses dynamic analysis for compiled python extensions, but static analysis elsewhere, if 'dynamic', then - dynamic analysis is used to parse all calldefs. + dynamic analysis is used to parse all calldefs. Defaults to 'auto'. Yields: Tuple[Dict[str, xdoctest.static_analysis.CallDefNode], str | ModuleType] - diff --git a/src/xdoctest/core.pyi b/src/xdoctest/core.pyi index 87c45780..ed30b0bf 100644 --- a/src/xdoctest/core.pyi +++ b/src/xdoctest/core.pyi @@ -13,10 +13,10 @@ DOCTEST_STYLES: Incomplete def parse_freeform_docstr_examples( docstr: str, - callname: str = None, - modpath: Union[str, PathLike] = None, + callname: Union[str, None] = None, + modpath: Union[str, PathLike, None] = None, lineno: int = 1, - fpath: Union[str, PathLike] = None, + fpath: Union[str, PathLike, None] = None, asone: bool = True ) -> Generator[xdoctest.doctest_example.DocTest, None, Any]: ... @@ -24,10 +24,10 @@ def parse_freeform_docstr_examples( def parse_google_docstr_examples( docstr: str, - callname: str = None, - modpath: Union[str, PathLike] = None, + callname: Union[str, None] = None, + modpath: Union[str, PathLike, None] = None, lineno: int = 1, - fpath: Union[str, PathLike] = None, + fpath: Union[str, PathLike, None] = None, eager_parse: bool = True ) -> Generator[xdoctest.doctest_example.DocTest, None, None]: ... @@ -40,12 +40,12 @@ def parse_auto_docstr_examples(docstr, *args, def parse_docstr_examples( docstr: str, - callname: str = None, - modpath: Union[str, PathLike] = None, + callname: Union[str, None] = None, + modpath: Union[str, PathLike, None] = None, lineno: int = 1, style: str = 'auto', - fpath: Union[str, PathLike] = None, - parser_kw: dict = ... + fpath: Union[str, PathLike, None] = None, + parser_kw: Union[dict, None] = None ) -> Generator[xdoctest.doctest_example.DocTest, None, None]: ... diff --git a/src/xdoctest/directive.py b/src/xdoctest/directive.py index 87beac4c..9f7d3f74 100644 --- a/src/xdoctest/directive.py +++ b/src/xdoctest/directive.py @@ -527,9 +527,9 @@ def effects(self, argv=None, environ=None): This is called by :func:`RuntimeState.update` to update itself Args: - argv (List[str], default=None): + argv (List[str] | None): if specified, overwrite sys.argv - environ (Dict[str, str], default=None): + environ (Dict[str, str] | None): if specified, overwrite os.environ Returns: @@ -667,14 +667,15 @@ def _is_requires_satisfied(arg, argv=None, environ=None): Args: arg (str): condition code - argv (List[str]): cmdline if arg is cmd code usually ``sys.argv`` - environ (Dict[str, str]): environment variables usually ``os.environ`` + argv (List[str] | None): cmdline if arg is cmd code usually ``sys.argv`` + environ (Dict[str, str] | None): environment variables usually ``os.environ`` Returns: bool: flag - True if the requirement is met Example: >>> from xdoctest.directive import * # NOQA + >>> from xdoctest.directive import _is_requires_satisfied >>> _is_requires_satisfied('PY2', argv=[]) >>> _is_requires_satisfied('PY3', argv=[]) >>> _is_requires_satisfied('cpython', argv=[]) diff --git a/src/xdoctest/directive.pyi b/src/xdoctest/directive.pyi index 7971c054..a1f0e86b 100644 --- a/src/xdoctest/directive.pyi +++ b/src/xdoctest/directive.pyi @@ -74,8 +74,8 @@ class Directive(utils.NiceRepr): ... def effects(self, - argv: List[str] = None, - environ: Dict[str, str] = None) -> List[Effect]: + argv: Union[List[str], None] = None, + environ: Union[Dict[str, str], None] = None) -> List[Effect]: ... diff --git a/src/xdoctest/doctest_example.pyi b/src/xdoctest/doctest_example.pyi index 3782977b..4bc891d5 100644 --- a/src/xdoctest/doctest_example.pyi +++ b/src/xdoctest/doctest_example.pyi @@ -16,7 +16,7 @@ class DoctestConfig(dict): def __init__(self, *args, **kwargs) -> None: ... - def getvalue(self, key: str, given: Any = None) -> Any: + def getvalue(self, key: str, given: Union[Any, None] = None) -> Any: ... @@ -80,22 +80,24 @@ class DocTest: linenos: bool = True, colored: Union[bool, None] = None, want: bool = True, - offset_linenos: bool = None, + offset_linenos: Union[bool, None] = None, prefix: bool = True) -> Generator[Any, None, None]: ... def format_src(self, linenos: bool = True, - colored: bool = None, + colored: Union[bool, None] = None, want: bool = True, - offset_linenos: bool = None, + offset_linenos: Union[bool, None] = None, prefix: bool = True) -> str: ... def anything_ran(self) -> bool: ... - def run(self, verbose: int = None, on_error: str = None) -> Dict: + def run(self, + verbose: Union[int, None] = None, + on_error: Union[str, None] = None) -> Dict: ... @property diff --git a/src/xdoctest/doctest_part.pyi b/src/xdoctest/doctest_part.pyi index 9735322a..16cbcdbc 100644 --- a/src/xdoctest/doctest_part.pyi +++ b/src/xdoctest/doctest_part.pyi @@ -58,15 +58,15 @@ class DoctestPart: def check(part, got_stdout: str, got_eval: str = ..., - runstate: directive.RuntimeState = None, - unmatched: list = None) -> None: + runstate: Union[directive.RuntimeState, None] = None, + unmatched: Union[list, None] = None) -> None: ... def format_part(self, linenos: bool = True, want: bool = True, startline: int = 1, - n_digits: int = None, + n_digits: Union[int, None] = None, colored: bool = False, partnos: bool = False, prefix: bool = True) -> str: diff --git a/src/xdoctest/exceptions.py b/src/xdoctest/exceptions.py index f4e1f60f..d22cdb1a 100644 --- a/src/xdoctest/exceptions.py +++ b/src/xdoctest/exceptions.py @@ -19,9 +19,9 @@ def __init__(self, msg, string=None, info=None, orig_ex=None): """ Args: msg (str): error message - string (str): the string that failed - info (Any): extra information - orig_ex (Exception): The underlying exceptoin + string (str | None): the string that failed + info (Any | None): extra information + orig_ex (Exception | None): The underlying exceptoin """ super(DoctestParseError, self).__init__(msg) self.msg = msg diff --git a/src/xdoctest/exceptions.pyi b/src/xdoctest/exceptions.pyi index 10989cdb..0e0baf95 100644 --- a/src/xdoctest/exceptions.pyi +++ b/src/xdoctest/exceptions.pyi @@ -1,3 +1,4 @@ +from typing import Union from typing import Any from typing import Any @@ -8,15 +9,15 @@ class MalformedDocstr(Exception): class DoctestParseError(Exception): msg: str - string: str - info: Any - orig_ex: Exception + string: Union[str, None] + info: Union[Any, None] + orig_ex: Union[Exception, None] def __init__(self, msg: str, - string: str = None, - info: Any = None, - orig_ex: Exception = None) -> None: + string: Union[str, None] = None, + info: Union[Any, None] = None, + orig_ex: Union[Exception, None] = None) -> None: ... diff --git a/src/xdoctest/plugin.pyi b/src/xdoctest/plugin.pyi index 4c5a89a8..44694715 100644 --- a/src/xdoctest/plugin.pyi +++ b/src/xdoctest/plugin.pyi @@ -40,10 +40,12 @@ class XDoctestItem(pytest.Item): obj: Incomplete fixture_request: Incomplete - def __init__(self, - name: str, - parent: Union[Any, None], - example: xdoctest.doctest_example.DocTest = None) -> None: + def __init__( + self, + name: str, + parent: Union[Any, None], + example: Union[xdoctest.doctest_example.DocTest, + None] = None) -> None: ... def setup(self) -> None: diff --git a/src/xdoctest/runner.pyi b/src/xdoctest/runner.pyi index 617d50fd..26dd4d13 100644 --- a/src/xdoctest/runner.pyi +++ b/src/xdoctest/runner.pyi @@ -24,12 +24,12 @@ def gather_doctests(doctest_identifiers, def doctest_module(module_identifier: Union[str, ModuleType, None] = None, - command: str = None, + command: Union[str, None] = None, argv: Union[List[str], None] = None, exclude: List[str] = ..., style: str = 'auto', verbose: Union[int, None] = None, - config: Dict[str, object] = None, + config: Union[Dict[str, object], None] = None, durations: Union[int, None] = None, analysis: str = 'auto') -> Dict[str, Any]: ... diff --git a/src/xdoctest/static_analysis.pyi b/src/xdoctest/static_analysis.pyi index ef72bf7f..21199f19 100644 --- a/src/xdoctest/static_analysis.pyi +++ b/src/xdoctest/static_analysis.pyi @@ -71,8 +71,9 @@ class TopLevelVisitor(ast.NodeVisitor): ... -def parse_static_calldefs(source: str = None, - fpath: str = None) -> Dict[str, CallDefNode]: +def parse_static_calldefs( + source: Union[str, None] = None, + fpath: Union[str, None] = None) -> Dict[str, CallDefNode]: ... @@ -82,8 +83,8 @@ def parse_calldefs(source: Incomplete | None = ..., def parse_static_value(key: str, - source: str = None, - fpath: str = None) -> object: + source: Union[str, None] = None, + fpath: Union[str, None] = None) -> object: ... diff --git a/src/xdoctest/utils/util_import.py b/src/xdoctest/utils/util_import.py index d7929f2d..24f4eb5f 100644 --- a/src/xdoctest/utils/util_import.py +++ b/src/xdoctest/utils/util_import.py @@ -26,8 +26,8 @@ def is_modname_importable(modname, sys_path=None, exclude=None): Args: modname (str): name of module to check - sys_path (list, default=None): if specified overrides ``sys.path`` - exclude (list): list of directory paths. if specified prevents these + sys_path (list | None, default=None): if specified overrides ``sys.path`` + exclude (list | None): list of directory paths. if specified prevents these directories from being searched. Returns: @@ -859,7 +859,7 @@ def modpath_to_modname(modpath, hide_init=True, hide_main=False, check=True, hide_main (bool, default=False): removes the __main__ suffix check (bool, default=True): if False, does not raise an error if modpath is a dir and does not contain an __init__ file. - relativeto (str, default=None): if specified, all checks are ignored + relativeto (str | None, default=None): if specified, all checks are ignored and this is considered the path to the root module. TODO: diff --git a/src/xdoctest/utils/util_import.pyi b/src/xdoctest/utils/util_import.pyi index 497720ce..b179667e 100644 --- a/src/xdoctest/utils/util_import.pyi +++ b/src/xdoctest/utils/util_import.pyi @@ -7,8 +7,8 @@ from _typeshed import Incomplete def is_modname_importable(modname: str, - sys_path: list = None, - exclude: list = None) -> bool: + sys_path: Union[list, None] = None, + exclude: Union[list, None] = None) -> bool: ... @@ -54,7 +54,7 @@ def modpath_to_modname(modpath: str, hide_init: bool = True, hide_main: bool = False, check: bool = True, - relativeto: str = None) -> str: + relativeto: Union[str, None] = None) -> str: ... diff --git a/tests/test_cases.py b/tests/test_cases.py index dec15e58..5845fd3b 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -113,7 +113,7 @@ def test(self, s): assert 'running 0 test' in text -def test_correct_skipping_on_decorators(): +def test_correct_skipping_on_decorators1(): """ This is a weird case similar to the torch dispatch doctest @@ -180,3 +180,46 @@ def dispatch(*types, **kwargs): runner.doctest_module(modpath, 'all', argv=[''], config=config) print(cap.text) assert '1 skipped' in cap.text + + +def test_correct_skipping_on_decorators_simple(): + """ + minimal test for decorator skips + """ + + import xdoctest + from xdoctest import runner + from os.path import join + + source = utils.codeblock( + ''' + def _my_decorator(): + """ + Example: + >>> # xdoctest: +SKIP + >>> @_my_decorator() + ... def my_func(x): + ... ... + >>> f(3) + """ + return + ''') + + config = { + 'style': 'google', + } + temp = utils.TempDir() + dpath = temp.ensure() + with temp as temp: + modpath = join(dpath, 'test_example_run.py') + + with open(modpath, 'w') as file: + file.write(source) + + examples = list(xdoctest.core.parse_doctestables(modpath, style='google', analysis='static')) + print(f'examples={examples}') + + with utils.CaptureStdout() as cap: + runner.doctest_module(modpath, 'all', argv=[''], config=config) + print(cap.text) + assert '1 skipped' in cap.text