diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 6177f25..9954784 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -8,8 +8,8 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: '3.12' - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 0e2190f..6ffd248 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -13,11 +13,11 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: main - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.github/workflows/run_tox.yml b/.github/workflows/run_tox.yml index 1407961..7fb6055 100644 --- a/.github/workflows/run_tox.yml +++ b/.github/workflows/run_tox.yml @@ -7,8 +7,8 @@ jobs: name: pre-commit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: '3.12' - uses: pre-commit/action@v3.0.1 @@ -19,12 +19,12 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41f915f..81f5dc1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.14.6 hooks: - id: ruff name: ruff unused imports diff --git a/.readthedocs.yml b/.readthedocs.yml index c55a20b..3dc6e2a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,9 +18,9 @@ formats: all build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.10" + python: "3.12" python: install: diff --git a/.ruff.toml b/.ruff.toml index a22554f..54880bf 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,7 +1,7 @@ indent-width = 4 line-length = 120 -target-version = "py38" +target-version = "py39" src = [ "src", @@ -30,9 +30,6 @@ ignore = [ # https://docs.astral.sh/ruff/rules/#flake8-bandit-s "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes - # https://docs.astral.sh/ruff/rules/#pyupgrade-up - "UP038", # Use X | Y in {} call instead of (X, Y) - # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} @@ -85,6 +82,10 @@ lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_l [lint.per-file-ignores] "setup.py" = ["PTH123"] +"__init__.py" = [ + "F401" # {name} imported but unused https://docs.astral.sh/ruff/rules/unused-import/ +] + "tests/*" = [ "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann @@ -94,4 +95,14 @@ lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_l # https://docs.astral.sh/ruff/rules/#refactor-r "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) + + # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt + "FBT003", # Boolean positional value in function call + + # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + "ARG001", # Unused function argument {name} + "ARG002", # Unused method argument {name} + "ARG003", # Unused class method argument: {name} + "ARG004", # Unused static method argument: {name} + "ARG005", # Unused lambda argument: {name} ] diff --git a/readme.md b/readme.md index deda47e..a57de33 100644 --- a/readme.md +++ b/readme.md @@ -36,6 +36,10 @@ This code will be executed ``` # Changelog +#### 0.17 (2026-03-18) +- Dropped support for Python 3.8 +- Added some type hints and updated dependencies + #### 0.16 (2025-04-01) - Fixed an issue where the ``hide`` and ``hide_output`` options were not working correctly diff --git a/requirements.txt b/requirements.txt index 82a11e3..9a4724b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,13 @@ # Dependencies for old python versions -pre-commit == 3.5.0; python_version < '3.9' pytest == 7.4.4; python_version < '3.10' sphinx == 7.4.7; python_version == '3.9' -sphinx == 7.1.2; python_version == '3.8' +sphinx == 8.1.3; python_version == '3.10' # Current dependencies -pre-commit == 4.0.1; python_version >= '3.9' pytest == 8.3.4; python_version >= '3.10' -sphinx == 8.1.3; python_version >= '3.10' +sphinx == 8.2.0; python_version >= '3.11' -ruff == 0.8.2 +pre-commit == 4.0.1 +ruff == 0.15.6 -sphinx-rtd-theme == 3.0.2 +sphinx-rtd-theme == 3.1.0 diff --git a/setup.py b/setup.py index 8a68664..3678828 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ -import typing from pathlib import Path from setuptools import find_packages, setup def load_version() -> str: - version: typing.Dict[str, str] = {} + version: dict[str, str] = {} with open('src/sphinx_exec_code/__version__.py') as fp: exec(fp.read(), version) assert version['__version__'], version @@ -15,7 +14,7 @@ def load_version() -> str: __version__ = load_version() print(f'Version: {__version__}') -print('') +print() # When we run tox tests we don't have these files available so we skip them readme = Path(__file__).with_name('readme.md') @@ -45,6 +44,7 @@ def load_version() -> str: }, package_dir={'': 'src'}, packages=find_packages('src', exclude=['tests*']), + install_requires=['typing-extensions'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -52,12 +52,12 @@ def load_version() -> str: 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: 3 :: Only', ], ) diff --git a/src/sphinx_exec_code/__version__.py b/src/sphinx_exec_code/__version__.py index 6c1078b..9446169 100644 --- a/src/sphinx_exec_code/__version__.py +++ b/src/sphinx_exec_code/__version__.py @@ -1 +1 @@ -__version__ = '0.16' +__version__ = '0.17' diff --git a/src/sphinx_exec_code/code_exec.py b/src/sphinx_exec_code/code_exec.py index 0b6251c..13cce45 100644 --- a/src/sphinx_exec_code/code_exec.py +++ b/src/sphinx_exec_code/code_exec.py @@ -1,13 +1,19 @@ +from __future__ import annotations + import os import subprocess import sys from itertools import dropwhile -from pathlib import Path +from typing import TYPE_CHECKING from sphinx_exec_code.code_exec_error import CodeExceptionError from sphinx_exec_code.configuration import PYTHONPATH_FOLDERS, SET_UTF8_ENCODING, WORKING_DIR +if TYPE_CHECKING: + from pathlib import Path + + def execute_code(code: str, file: Path, first_loc: int) -> str: cwd: Path = WORKING_DIR.value encoding = 'utf-8' if SET_UTF8_ENCODING.value else None @@ -21,14 +27,14 @@ def execute_code(code: str, file: Path, first_loc: int) -> str: except KeyError: env['PYTHONPATH'] = os.pathsep.join(python_folders) - run = subprocess.run([sys.executable, '-c', code.encode('utf-8')], capture_output=True, text=True, + run = subprocess.run([sys.executable, '-c', code.encode('utf-8')], capture_output=True, text=True, # noqa: S603 encoding=encoding, cwd=cwd, env=env, check=False) if run.returncode != 0: raise CodeExceptionError(code, file, first_loc, run.returncode, run.stderr) from None # decode output and drop tailing spaces - ret_str = (run.stdout if run.stdout is not None else '' + run.stderr if run.stderr is not None else '').rstrip() + ret_str = ((run.stdout or '') + (run.stderr or '')).rstrip() # drop leading empty lines ret_lines = list(dropwhile(lambda x: not x.strip(), ret_str.splitlines())) diff --git a/src/sphinx_exec_code/code_exec_error.py b/src/sphinx_exec_code/code_exec_error.py index ed7f38d..bbbce58 100644 --- a/src/sphinx_exec_code/code_exec_error.py +++ b/src/sphinx_exec_code/code_exec_error.py @@ -1,7 +1,11 @@ +from __future__ import annotations + import re -from pathlib import Path -from typing import List +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path re_line = re.compile(r'^\s*File "()", line (\d+), in ', re.MULTILINE) @@ -16,14 +20,14 @@ def __init__(self, code: str, file: Path, first_loc: int, ret: int, stderr: str) self.exec_ret = ret self.exec_err = stderr - def _err_line(self, lines: List[str]) -> int: + def _err_line(self, lines: list[str]) -> int: # Find the last line where the error happened err_line = len(lines) for m in re_line.finditer(self.exec_err): err_line = int(m.group(2)) return err_line - 1 - def pformat(self) -> List[str]: + def pformat(self) -> list[str]: filename = self.file.name code_lines = self.code.splitlines() err_line = self._err_line(code_lines) @@ -41,7 +45,6 @@ def pformat(self) -> List[str]: for tb_line in self.exec_err.splitlines(): m = re_line.search(tb_line) if m: - tb_line = tb_line.replace('File ""', f'File "{filename}"') tb_line = tb_line.replace('File ""', f'File "{filename}"') tb_line = tb_line.replace(f', line {m.group(2)}, in ', f', line {int(m.group(2)) + self.first_loc - 1}') diff --git a/src/sphinx_exec_code/code_format.py b/src/sphinx_exec_code/code_format.py index 0530e5e..73aeb8c 100644 --- a/src/sphinx_exec_code/code_format.py +++ b/src/sphinx_exec_code/code_format.py @@ -1,7 +1,11 @@ +from __future__ import annotations + from textwrap import dedent -from typing import List, Tuple +from typing import TYPE_CHECKING + -from docutils.statemachine import StringList +if TYPE_CHECKING: + from docutils.statemachine import StringList class VisibilityMarkerError(Exception): @@ -19,7 +23,7 @@ def __init__(self, marker: str) -> None: self.do_add = True self.skip_empty = False - self.lines: List[str] = [] + self.lines: list[str] = [] def is_marker(self, line: str) -> bool: if line == self.start: @@ -54,7 +58,7 @@ def add_line(self, line: str) -> None: self.lines.append(line) - def get_lines(self) -> List[str]: + def get_lines(self) -> list[str]: # remove leading and tailing empty lines of the code code_lines = self.lines while code_lines and not code_lines[0].strip(): @@ -64,7 +68,7 @@ def get_lines(self) -> List[str]: return code_lines -def get_show_exec_code(code_lines: StringList) -> Tuple[str, str]: +def get_show_exec_code(code_lines: StringList | list[str]) -> tuple[str, str]: shown = CodeMarker('hide') executed = CodeMarker('skip') diff --git a/src/sphinx_exec_code/configuration/base.py b/src/sphinx_exec_code/configuration/base.py index 717e578..5d38f2b 100644 --- a/src/sphinx_exec_code/configuration/base.py +++ b/src/sphinx_exec_code/configuration/base.py @@ -1,20 +1,25 @@ -from typing import Any, Final, Generic, Optional, Tuple, Type, TypeVar, Union +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar -from sphinx.application import Sphinx as SphinxApp from sphinx.errors import ConfigError from sphinx_exec_code.__const__ import log +if TYPE_CHECKING: + from sphinx.application import Sphinx as SphinxApp + + TYPE_VALUE = TypeVar('TYPE_VALUE') class SphinxConfigValue(Generic[TYPE_VALUE]): - SPHINX_TYPE: Union[Tuple[Type[Any], ...], Type[Any]] + SPHINX_TYPE: tuple[type[Any], ...] | type[Any] | tuple[()] - def __init__(self, sphinx_name: str, initial_value: Optional[TYPE_VALUE] = None) -> None: + def __init__(self, sphinx_name: str, initial_value: TYPE_VALUE | None = None) -> None: self.sphinx_name: Final = sphinx_name - self._value: Optional[TYPE_VALUE] = initial_value + self._value: TYPE_VALUE | None = initial_value @property def value(self) -> TYPE_VALUE: @@ -26,7 +31,7 @@ def value(self) -> TYPE_VALUE: def transform_value(self, app: SphinxApp, value: Any) -> TYPE_VALUE: return value - def validate_value(self, value) -> TYPE_VALUE: + def validate_value(self, value: Any) -> TYPE_VALUE: return value def from_app(self, app: SphinxApp) -> TYPE_VALUE: diff --git a/src/sphinx_exec_code/configuration/flag_config.py b/src/sphinx_exec_code/configuration/flag_config.py index 2471e1c..919df0e 100644 --- a/src/sphinx_exec_code/configuration/flag_config.py +++ b/src/sphinx_exec_code/configuration/flag_config.py @@ -1,15 +1,25 @@ -from sphinx.application import Sphinx as SphinxApp +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from typing_extensions import override from sphinx_exec_code.configuration.base import SphinxConfigValue +if TYPE_CHECKING: + from sphinx.application import Sphinx as SphinxApp + + class SphinxConfigFlag(SphinxConfigValue[bool]): SPHINX_TYPE = bool - def validate_value(self, value) -> bool: + @override + def validate_value(self, value: Any) -> bool: if not isinstance(value, bool): raise TypeError() return value - def transform_value(self, app: SphinxApp, value) -> bool: + @override + def transform_value(self, app: SphinxApp, value: Any) -> bool: return bool(value) diff --git a/src/sphinx_exec_code/configuration/path_config.py b/src/sphinx_exec_code/configuration/path_config.py index eb12d46..1835080 100644 --- a/src/sphinx_exec_code/configuration/path_config.py +++ b/src/sphinx_exec_code/configuration/path_config.py @@ -1,20 +1,25 @@ +from __future__ import annotations + from pathlib import Path -from typing import Tuple +from typing import TYPE_CHECKING, Any, Final -from sphinx.application import Sphinx as SphinxApp +from typing_extensions import override from sphinx_exec_code.__const__ import log from sphinx_exec_code.configuration.base import TYPE_VALUE, SphinxConfigValue +if TYPE_CHECKING: + from sphinx.application import Sphinx as SphinxApp + + class InvalidPathError(Exception): pass -class SphinxConfigPath(SphinxConfigValue[TYPE_VALUE]): - SPHINX_TYPE = (str, Path) +class _SphinxConfigPathBase(SphinxConfigValue[TYPE_VALUE]): - def make_path(self, app: SphinxApp, value) -> Path: + def make_path(self, app: SphinxApp, value: Any) -> Path: try: path = Path(value) except Exception: @@ -33,28 +38,36 @@ def check_folder_exists(self, folder: Path) -> Path: return folder -class SphinxConfigFolder(SphinxConfigPath[Path]): - def transform_value(self, app: SphinxApp, value) -> Path: +class SphinxConfigFolder(_SphinxConfigPathBase[Path]): + SPHINX_TYPE = (str, Path) + + @override + def transform_value(self, app: SphinxApp, value: Any) -> Path: return self.make_path(app, value) + @override def validate_value(self, value: Path) -> Path: return self.check_folder_exists(value) -class SphinxConfigMultipleFolderStr(SphinxConfigPath[Tuple[str, ...]]): +class SphinxConfigMultipleFolderStr(_SphinxConfigPathBase[tuple[str, ...]]): SPHINX_TYPE = () - def transform_value(self, app: SphinxApp, value) -> Tuple[Path, ...]: - return tuple(self.make_path(app, p) for p in value) + @override + def transform_value(self, app: SphinxApp, value: Any) -> tuple[str, ...]: + return tuple(str(self.make_path(app, p)) for p in value) + + @override + def validate_value(self, value: Any) -> tuple[str, ...]: + _path_value: Final = tuple(Path(v) for v in value) - def validate_value(self, value: Tuple[Path, ...]) -> Tuple[str, ...]: # check that folders exist - for f in value: + for f in _path_value: self.check_folder_exists(f) # Search for a python package and print a warning if we find none # since this is the only reason to specify additional folders - for f in value: + for f in _path_value: package_found = False for _f in f.iterdir(): if not _f.is_dir(): @@ -71,4 +84,4 @@ def validate_value(self, value: Tuple[Path, ...]) -> Tuple[str, ...]: if not package_found: log.warning(f'[exec-code] No Python packages found in {f}') - return tuple(map(str, value)) + return value diff --git a/src/sphinx_exec_code/configuration/values.py b/src/sphinx_exec_code/configuration/values.py index 16df932..d9ab5a1 100644 --- a/src/sphinx_exec_code/configuration/values.py +++ b/src/sphinx_exec_code/configuration/values.py @@ -1,10 +1,12 @@ +from typing import Final + from .flag_config import SphinxConfigFlag from .path_config import SphinxConfigFolder, SphinxConfigMultipleFolderStr -EXAMPLE_DIR = SphinxConfigFolder('exec_code_example_dir') +EXAMPLE_DIR: Final = SphinxConfigFolder('exec_code_example_dir') # Options for code execution -WORKING_DIR = SphinxConfigFolder('exec_code_working_dir') -PYTHONPATH_FOLDERS = SphinxConfigMultipleFolderStr('exec_code_source_folders') -SET_UTF8_ENCODING = SphinxConfigFlag('exec_code_set_utf8_encoding') +WORKING_DIR: Final = SphinxConfigFolder('exec_code_working_dir') +PYTHONPATH_FOLDERS: Final = SphinxConfigMultipleFolderStr('exec_code_source_folders') +SET_UTF8_ENCODING: Final = SphinxConfigFlag('exec_code_set_utf8_encoding') diff --git a/src/sphinx_exec_code/sphinx_api.py b/src/sphinx_exec_code/sphinx_api.py index fb13f0d..040fe22 100644 --- a/src/sphinx_exec_code/sphinx_api.py +++ b/src/sphinx_exec_code/sphinx_api.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import os from pathlib import Path -from typing import Any, Dict - -from sphinx.application import Sphinx as SphinxApp +from typing import TYPE_CHECKING, Any, Final from sphinx_exec_code import __version__ from sphinx_exec_code.__const__ import log @@ -11,6 +11,10 @@ from .configuration import EXAMPLE_DIR, PYTHONPATH_FOLDERS, SET_UTF8_ENCODING, WORKING_DIR +if TYPE_CHECKING: + from sphinx.application import Sphinx as SphinxApp + + def builder_ready(app: SphinxApp) -> None: # load configuration EXAMPLE_DIR.from_app(app) @@ -19,20 +23,21 @@ def builder_ready(app: SphinxApp) -> None: SET_UTF8_ENCODING.from_app(app) -def setup(app) -> Dict[str, Any]: +def setup(app: SphinxApp) -> dict[str, Any]: """ Register sphinx_execute_code directive with Sphinx """ - confdir = Path(app.confdir) + confdir: Final = Path(app.confdir) - code_folders = [] - src_dir = confdir.with_name('src') + # automatically detect add src/ + code_folders: Final[list[str]] = [] + src_dir: Final = confdir.with_name('src') if src_dir.is_dir(): - code_folders.append(src_dir) + code_folders.append(str(src_dir)) # Configuration options EXAMPLE_DIR.add_config_value(app, confdir) WORKING_DIR.add_config_value(app, confdir.parent) - PYTHONPATH_FOLDERS.add_config_value(app, code_folders) + PYTHONPATH_FOLDERS.add_config_value(app, tuple(code_folders)) SET_UTF8_ENCODING.add_config_value(app, os.name == 'nt') app.connect('builder-inited', builder_ready) @@ -44,6 +49,6 @@ def setup(app) -> Dict[str, Any]: 'version': __version__, # https://github.com/spacemanspiff2007/sphinx-exec-code/issues/2 - # This extension does not store any states making it safe for parallel reading + # This extension does not store any global states making it safe for parallel reading 'parallel_read_safe': True } diff --git a/src/sphinx_exec_code/sphinx_exec.py b/src/sphinx_exec_code/sphinx_exec.py index dd088fb..c33ab3d 100644 --- a/src/sphinx_exec_code/sphinx_exec.py +++ b/src/sphinx_exec_code/sphinx_exec.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import traceback from pathlib import Path +from typing import TYPE_CHECKING, Final from docutils.statemachine import StringList from sphinx.directives.code import CodeBlock @@ -13,6 +16,10 @@ from sphinx_exec_code.sphinx_spec import SphinxSpecBase, build_spec, get_specs +if TYPE_CHECKING: + from docutils.nodes import Node + + class ExecCode(SphinxDirective): """ Sphinx class for execute_code directive """ @@ -39,7 +46,7 @@ def run(self) -> list: msg = f'Error while running {name}!' raise ExtensionError(msg, orig_exc=e) from None - def _get_code_line(self, line_no: int, content: StringList) -> int: + def _get_code_line(self, line_no: int, content: StringList | list[str]) -> int: """Get the first line number of the code""" if not content: return line_no @@ -54,13 +61,13 @@ def _get_code_line(self, line_no: int, content: StringList) -> int: return line_no + i - def _run(self) -> list: + def _run(self) -> list[Node]: """ Executes python code for an RST document, taking input from content or from a filename :return: """ - output = [] + output: Final[list[Node]] = [] raw_file, raw_line = self.get_source_info() - content = self.content + content: StringList | list[str] = self.content file = Path(raw_file) line = self._get_code_line(raw_line, content) @@ -90,8 +97,8 @@ def _run(self) -> list: print() # Log pretty message - for line in e.pformat(): - log.error(line) + for _line in e.pformat(): + log.error(_line) msg = 'Could not execute code!' raise ExtensionError(msg) from None @@ -100,7 +107,7 @@ def _run(self) -> list: self.create_literal_block(output, code_results, output_spec, line) return output - def create_literal_block(self, objs: list, code: str, spec: SphinxSpecBase, line: int) -> None: + def create_literal_block(self, objs: list[Node], code: str, spec: SphinxSpecBase, line: int) -> None: if spec.hide or not code: return None diff --git a/src/sphinx_exec_code/sphinx_spec.py b/src/sphinx_exec_code/sphinx_spec.py index d3e932e..886d487 100644 --- a/src/sphinx_exec_code/sphinx_spec.py +++ b/src/sphinx_exec_code/sphinx_spec.py @@ -1,15 +1,19 @@ -from typing import Any, ClassVar, Dict, Final, Tuple +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Final -from docutils.nodes import literal_block from docutils.parsers.rst import directives from sphinx.directives.code import CodeBlock -from sphinx.util.typing import OptionSpec +from typing_extensions import Self, override + -from sphinx_exec_code.__const__ import log +if TYPE_CHECKING: + from docutils.nodes import literal_block + from sphinx.util.typing import OptionSpec class SphinxSpecBase: - defaults: ClassVar[Dict[str, str]] + defaults: ClassVar[dict[str, str | bool]] @staticmethod def alias_to_name(alias: str, *, do_log: bool = True) -> str: @@ -20,10 +24,10 @@ def name_to_alias(name: str) -> str: raise NotImplementedError() @staticmethod - def post_process_spec(spec: Dict[str, Any], options: Dict[str, Any]) -> None: + def post_process_spec(spec: dict[str, Any], options: dict[str, Any]) -> None: raise NotImplementedError() - def __init__(self, spec: Dict[str, Any]) -> None: + def __init__(self, spec: dict[str, Any]) -> None: self.hide: Final = spec.pop('hide', '') is None self.language: Final = spec.pop('language') self.spec: Final = spec @@ -34,7 +38,7 @@ def set_block_spec(self, block: literal_block) -> None: return None @classmethod - def from_options(cls, options: Dict[str, Any]) -> 'SphinxSpecBase': + def from_options(cls, options: dict[str, Any]) -> Self: spec_names = tuple(cls.create_spec().keys()) spec = {cls.alias_to_name(n, do_log=False): v for n, v in cls.defaults.items()} @@ -75,7 +79,7 @@ def build_spec() -> OptionSpec: return spec -def get_specs(options: Dict[str, Any]) -> Tuple['SpecCode', 'SpecOutput']: +def get_specs(options: dict[str, Any]) -> tuple[SpecCode, SpecOutput]: supported = set(SpecCode.create_spec()) | set(SpecOutput.create_spec()) invalid = set(options) - supported @@ -92,28 +96,26 @@ def get_specs(options: Dict[str, Any]) -> Tuple['SpecCode', 'SpecOutput']: class SpecCode(SphinxSpecBase): defaults: ClassVar = { 'filename': '', - 'hide_code': False, # deprecated 2024 - remove after some time, must come before the new hide flag! 'hide': False, 'language': 'python', } + @override @staticmethod def alias_to_name(alias: str, *, do_log: bool = True) -> str: - if alias == 'hide_code': - if do_log: - log.warning('The "hide_code" directive is deprecated! Use "hide" instead!') - return 'hide' return alias + @override @staticmethod def name_to_alias(name: str) -> str: return name + @override @staticmethod - def post_process_spec(spec: Dict[str, Any], options: Dict[str, Any]) -> None: + def post_process_spec(spec: dict[str, Any], options: dict[str, Any]) -> None: return None - def __init__(self, **kwargs: Dict[str, Any]) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.filename: Final[str] = self.spec.pop('filename') @@ -124,20 +126,23 @@ class SpecOutput(SphinxSpecBase): 'language': 'none', } + @override @staticmethod - def alias_to_name(alias: str, *, do_log: bool = True) -> str: # noqa: ARG004 + def alias_to_name(alias: str, *, do_log: bool = True) -> str: if alias.endswith('_output'): return alias[:-7] return alias + @override @staticmethod def name_to_alias(name: str) -> str: if name.endswith('_output'): return name return name + '_output' + @override @staticmethod - def post_process_spec(spec: Dict[str, Any], options: Dict[str, Any]) -> None: + def post_process_spec(spec: dict[str, Any], options: dict[str, Any]) -> None: # if we have a name for code but not for output we programmatically build it by appending the _output suffix name_output = SpecOutput.name_to_alias('name') if name_output in options: @@ -149,3 +154,4 @@ def post_process_spec(spec: Dict[str, Any], options: Dict[str, Any]) -> None: # if we have a name for input we create a name for output spec['name'] = f'{options[name_code]:s}_output' + return None diff --git a/tests/test_code_format.py b/tests/test_code_format.py index 05edb39..1e2d77c 100644 --- a/tests/test_code_format.py +++ b/tests/test_code_format.py @@ -57,3 +57,20 @@ def test_code_no_exec() -> None: show, run = get_show_exec_code(code.splitlines()) assert show == 'print(1 / 0)\nprint(2 / 0)' assert run == '' + + +def test_code_comment_indent() -> None: + code = ''' +# hide: start +def test(): +# hide: stop + # comment + print('asdf') + +# hide: start +test() +# hide: stop +''' + show, run = get_show_exec_code(code.splitlines()) + assert show == "# comment\nprint('asdf')" + assert run == "def test():\n # comment\n print('asdf')\n\ntest()" diff --git a/tests/test_sphinx_spec.py b/tests/test_sphinx_spec.py index 12a0d5b..e35086f 100644 --- a/tests/test_sphinx_spec.py +++ b/tests/test_sphinx_spec.py @@ -16,9 +16,6 @@ def test_spec_code() -> None: assert obj.filename == 'filename' assert obj.spec == {'caption': 'my_header', 'linenos': True} - obj = SpecCode.from_options({'hide_code': None}) - assert obj.hide is True - obj = SpecCode.from_options({'hide': None}) assert obj.hide is True @@ -40,7 +37,7 @@ def test_invalid_options() -> None: assert str(e.value) == ( 'Invalid option: hide-output! ' 'Supported: caption, caption_output, class, class_output, dedent, dedent_output, ' - 'emphasize-lines, emphasize-lines_output, filename, force, force_output, hide, hide_code, hide_output, ' + 'emphasize-lines, emphasize-lines_output, filename, force, force_output, hide, hide_output, ' 'language, language_output, lineno-start, lineno-start_output, linenos, linenos_output, name, name_output' ) @@ -50,7 +47,7 @@ def test_invalid_options() -> None: assert str(e.value) == ( 'Invalid options: caption-output, hide-output! ' 'Supported: caption, caption_output, class, class_output, dedent, dedent_output, ' - 'emphasize-lines, emphasize-lines_output, filename, force, force_output, hide, hide_code, hide_output, ' + 'emphasize-lines, emphasize-lines_output, filename, force, force_output, hide, hide_output, ' 'language, language_output, lineno-start, lineno-start_output, linenos, linenos_output, name, name_output' ) diff --git a/tox.ini b/tox.ini index de74699..7e36e6a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,22 @@ [tox] envlist = - py38 py39 py310 py311 py312 py313 + py314 docs [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312, docs 3.13: py313 + 3.14: py314 [testenv]