diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml index 5c1736d3c5b..f2055e2798f 100644 --- a/.github/workflows/builddoc.yml +++ b/.github/workflows/builddoc.yml @@ -17,7 +17,7 @@ env: UV_SYSTEM_PYTHON: "1" # make uv do global installs jobs: - build: + verbose: runs-on: ubuntu-latest steps: @@ -40,7 +40,7 @@ jobs: run: > sphinx-build -M html ./doc ./build/sphinx - -vv + --verbose --jobs=auto --show-traceback --fail-on-warning diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a50c0415d62..27f67597d46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,11 +29,11 @@ jobs: ubuntu: runs-on: ubuntu-latest name: Python ${{ matrix.python }} (Docutils ${{ matrix.docutils }}) + timeout-minutes: 15 strategy: fail-fast: false matrix: python: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -71,9 +71,9 @@ jobs: PYTHONWARNINGS: "error" # treat all warnings as errors deadsnakes: - if: false runs-on: ubuntu-latest name: Python ${{ matrix.python }} (Docutils ${{ matrix.docutils }}) + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -96,6 +96,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + sed -i 's/flit_core>=3.7/flit_core @ git+https:\/\/github.com\/pypa\/flit.git#subdirectory=flit_core/' pyproject.toml python -m pip install .[test] - name: Install Docutils ${{ matrix.docutils }} run: python -m pip install --upgrade "docutils~=${{ matrix.docutils }}.0" @@ -104,18 +105,48 @@ jobs: env: PYTHONWARNINGS: "error" # treat all warnings as errors - deadsnakes-free-threraded: - if: false + free-threaded: runs-on: ubuntu-latest - name: Python ${{ matrix.python }} (Docutils ${{ matrix.docutils }}; free-threaded) + name: Python ${{ matrix.python }} (free-threaded) + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python: + - "3.13" + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python }} (deadsnakes) + uses: deadsnakes/action@v3.2.0 + with: + python-version: ${{ matrix.python }} + nogil: true + - name: Check Python version + run: python --version --version + - name: Install graphviz + run: sudo apt-get install graphviz + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install .[test] + # markupsafe._speedups has not declared that it can run safely without the GIL + - name: Remove markupsafe._speedups + run: rm -rf "$(python -c 'from markupsafe._speedups import __file__ as f; print(f)')" + - name: Test with pytest + run: python -m pytest -vv --durations 25 + env: + PYTHONWARNINGS: "error" # treat all warnings as errors + + deadsnakes-free-threaded: + runs-on: ubuntu-latest + name: Python ${{ matrix.python }} (free-threaded) + timeout-minutes: 15 strategy: fail-fast: false matrix: python: - "3.14" - docutils: - - "0.20" - - "0.21" steps: - uses: actions/checkout@v4 @@ -131,9 +162,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + sed -i 's/flit_core>=3.7/flit_core @ git+https:\/\/github.com\/pypa\/flit.git#subdirectory=flit_core/' pyproject.toml python -m pip install .[test] - - name: Install Docutils ${{ matrix.docutils }} - run: python -m pip install --upgrade "docutils~=${{ matrix.docutils }}.0" # markupsafe._speedups has not declared that it can run safely without the GIL - name: Remove markupsafe._speedups run: rm -rf "$(python -c 'from markupsafe._speedups import __file__ as f; print(f)')" @@ -145,6 +175,7 @@ jobs: windows: runs-on: windows-2019 name: Windows + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -170,6 +201,7 @@ jobs: docutils-latest: runs-on: ubuntu-latest name: Docutils HEAD + timeout-minutes: 15 steps: - name: Install epubcheck @@ -207,6 +239,7 @@ jobs: oldest-supported: runs-on: ubuntu-latest name: Oldest supported + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -236,6 +269,7 @@ jobs: latex: runs-on: ubuntu-latest name: LaTeX + timeout-minutes: 15 container: image: ghcr.io/sphinx-doc/sphinx-ci @@ -265,6 +299,7 @@ jobs: if: github.event_name == 'push' && github.repository_owner == 'sphinx-doc' runs-on: ubuntu-latest name: Coverage + timeout-minutes: 15 steps: - uses: actions/checkout@v4 diff --git a/.ruff.toml b/.ruff.toml index 663382a98f7..e34aed8f70a 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,4 +1,4 @@ -target-version = "py310" # Pin Ruff to Python 3.10 +target-version = "py311" # Pin Ruff to Python 3.11 line-length = 88 output-format = "full" @@ -7,7 +7,6 @@ extend-exclude = [ "tests/js/roots/*", "build/*", "doc/_build/*", -# "sphinx/search/*", "doc/usage/extensions/example*.py", ] @@ -16,15 +15,51 @@ preview = true ignore = [ # flake8-annotations "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `{name}` + # flake8-unused-arguments ('ARG') + "ARG001", # Unused function argument: `{name}` + "ARG002", # Unused method argument: `{name}` + "ARG003", # Unused class method argument: `{name}` + "ARG005", # Unused lambda argument: `{name}` + # flake8-commas ('COM') + "COM812", # Trailing comma missing # pycodestyle "E741", # Ambiguous variable name: `{name}` # pyflakes "F841", # Local variable `{name}` is assigned to but never used + # flake8-logging-format + "G003", # Logging statement uses `+` # refurb "FURB101", # `open` and `read` should be replaced by `Path(...).read_text(...)` "FURB103", # `open` and `write` should be replaced by `Path(...).write_text(...)` - # pylint - "PLC1901", # simplify truthy/falsey string comparisons + # perflint + "PERF203", # `try`-`except` within a loop incurs performance overhead + # flake8-pie ('PIE') + "PIE790", # Unnecessary `pass` statement + # pylint ('PLC') + "PLC0415", # `import` should be at the top-level of a file + "PLC2701", # Private name import `{name}` from external module `{module}` + # pylint ('PLR') + "PLR0904", # Too many public methods ({methods} > {max_methods}) + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) + "PLR0914", # Too many local variables ({current_amount}/{max_amount}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR0916", # Too many Boolean expressions ({expressions} > {max_expressions}) + "PLR0917", # Too many positional arguments ({c_pos}/{max_pos}) + "PLR1702", # Too many nested blocks ({nested_blocks} > {max_nested_blocks}) + "PLR2004", # Magic value used in comparison, consider replacing `{value}` with a constant variable + "PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation + "PLR6104", # Use `{operator}` to perform an augmented assignment directly + "PLR6301", # Method `{method_name}` could be a function, class method, or static method + # pylint ('PLW') + "PLW2901", # Outer {outer_kind} variable `{name}` overwritten by inner {inner_kind} target + # flake8-bandit ('S') + "S101", # Use of `assert` detected + "S110", # `try`-`except`-`pass` detected, consider logging the exception + "S404", # `subprocess` module is possibly insecure + "S405", # `xml.etree` methods are vulnerable to XML attacks + "S603", # `subprocess` call: check for execution of untrusted input # flake8-simplify "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM108", # Use ternary operator `{contents}` instead of `if`-`else`-block @@ -44,7 +79,7 @@ select = [ # flake8-annotations ('ANN') "ANN", # flake8-unused-arguments ('ARG') - "ARG004", # Unused static method argument: `{name}` + "ARG", # flake8-async ('ASYNC') "ASYNC", # flake8-bugbear ('B') @@ -56,8 +91,7 @@ select = [ # mccabe ('C90') # "C901", # `{name}` is too complex ({complexity} > {max_complexity}) # flake8-commas ('COM') - "COM818", # Trailing comma on bare tuple prohibited - "COM819", # Trailing comma prohibited + "COM", # Trailing comma prohibited # flake8-copyright ('CPY') # NOT YET USED # pydocstyle ('D') @@ -122,6 +156,8 @@ select = [ "F", # flake8-future-annotations ('FA') "FA", + # flake8-fastapi ('FAST') + # FastAPI is not used in Sphinx # flake8-boolean-trap ('FBT') # NOT YET USED # flake8-fixme ('FIX') @@ -131,14 +167,7 @@ select = [ # refurb ('FURB') "FURB", # flake8-logging-format ('G') - "G001", # Logging statement uses `str.format` - "G002", # Logging statement uses `%` -# "G003", # Logging statement uses `+` - "G004", # Logging statement uses f-string - "G010", # Logging statement uses `warn` instead of `warning` - "G101", # Logging statement uses an `extra` field that clashes with a `LogRecord` field: `{key}` - "G201", # Logging `.exception(...)` should be used instead of `.error(..., exc_info=True)` - "G202", # Logging statement has redundant `exc_info` + "G", # isort ('I') "I", # flake8-import-conventions ('ICN') @@ -158,94 +187,19 @@ select = [ # pandas-vet ('PD') # Pandas is not used in Sphinx # perflint ('PERF') - "PERF101", # Do not cast an iterable to `list` before iterating over it - "PERF102", # When using only the {subset} of a dict use the `{subset}()` method -# "PERF203", # `try`-`except` within a loop incurs performance overhead - "PERF401", # Use a list comprehension to create a transformed list - "PERF402", # Use `list` or `list.copy` to create a copy of a list - "PERF403", # Use a dictionary comprehension instead of a for-loop + "PERF", # pygrep-hooks ('PGH') "PGH", # flake8-pie ('PIE') -# "PIE790", # Unnecessary `pass` statement - "PIE794", # Class field `{name}` is defined multiple times - "PIE796", # Enum contains duplicate value: `{value}` - "PIE800", # Unnecessary spread `**` - "PIE804", # Unnecessary `dict` kwargs - "PIE807", # Prefer `list` over useless lambda - "PIE810", # Call `{attr}` once with a `tuple` - # pylint ('PLC') - "PLC0105", # `{kind}` name "{param_name}" does not reflect its {variance}; consider renaming it to "{replacement_name}" - "PLC0131", # `{kind}` cannot be both covariant and contravariant - "PLC0132", # `{kind}` name `{param_name}` does not match assigned variable name `{var_name}` - "PLC0205", # Class `__slots__` should be a non-string iterable - "PLC0208", # Use a sequence type instead of a `set` when iterating over values - "PLC0414", # Import alias does not rename original package -# "PLC0415", # `import` should be at the top-level of a file - "PLC1901", # `{existing}` can be simplified to `{replacement}` as an empty string is falsey - "PLC2401", # {kind} name `{name}` contains a non-ASCII character, consider renaming it - "PLC2403", # Module alias `{name}` contains a non-ASCII character, use an ASCII-only alias -# "PLC2701", # Private name import `{name}` from external module `{module}` - "PLC2801", # Unnecessary dunder call to `{method}`. {replacement}. - "PLC3002", # Lambda expression called directly. Execute the expression inline instead. - # pylint ('PLE') - "PLE0100", # `__init__` method is a generator - "PLE0101", # Explicit return in `__init__` - "PLE0116", # `continue` not supported inside `finally` clause - "PLE0117", # Nonlocal name `{name}` found without binding - "PLE0118", # Name `{name}` is used prior to global declaration on line {line} - "PLE0241", # Duplicate base `{base}` for class `{class}` - "PLE0302", # The special method `{}` expects {}, {} {} given - "PLE0307", # `__str__` does not return `str` - "PLE0604", # Invalid object in `__all__`, must contain only strings - "PLE0605", # Invalid format for `__all__`, must be `tuple` or `list` - "PLE1142", # `await` should be used within an async function - "PLE1205", # Too many arguments for `logging` format string - "PLE1206", # Not enough arguments for `logging` format string - "PLE1300", # Unsupported format character '{}' - "PLE1307", # Format type does not match argument type - "PLE1310", # String `{strip}` call contains duplicate characters (did you mean `{removal}`?) - "PLE1507", # Invalid type for initial `os.getenv` argument; expected `str` - "PLE1700", # `yield from` statement in async function; use `async for` instead - "PLE2502", # Contains control characters that can permit obfuscated code - "PLE2510", # Invalid unescaped character backspace, use "\b" instead - "PLE2512", # Invalid unescaped character SUB, use "\x1A" instead - "PLE2513", # Invalid unescaped character ESC, use "\x1B" instead - "PLE2514", # Invalid unescaped character NUL, use "\0" instead - "PLE2515", # Invalid unescaped character zero-width-space, use "\u200B" instead - # pylint ('PLR') -# "PLR0124", # Name compared with itself, consider replacing `{left} {} {right}` - "PLR0133", # Two constants compared in a comparison, consider replacing `{left_constant} {} {right_constant}` - "PLR0206", # Cannot have defined parameters for properties - "PLR0402", # Use `from {module} import {name}` in lieu of alias -# "PLR0911", # Too many return statements ({returns} > {max_returns}) -# "PLR0912", # Too many branches ({branches} > {max_branches}) -# "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) -# "PLR0915", # Too many statements ({statements} > {max_statements}) - "PLR1711", # Useless `return` statement at end of function -# "PLR1714", # Consider merging multiple comparisons: `{expr}`. Use a `set` if the elements are hashable. - "PLR1722", # Use `sys.exit()` instead of `{name}` -# "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable -# "PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation - # pylint ('PLW') - "PLW0120", # `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it - "PLW0127", # Self-assignment of variable `{name}` - "PLW0129", # Asserting on an empty string literal will never pass - "PLW0131", # Named expression used without context - "PLW0406", # Module `{name}` imports itself - "PLW0602", # Using global for `{name}` but no assignment is done -# "PLW0603", # Using the global statement to update `{name}` is discouraged - "PLW0711", # Exception to catch is the result of a binary `and` operation - "PLW1508", # Invalid type for environment variable default; expected `str` or `None` - "PLW1509", # `preexec_fn` argument is unsafe when using threads -# "PLW2901", # Outer {outer_kind} variable `{name}` overwritten by inner {inner_kind} target - "PLW3301", # Nested `{}` calls can be flattened + "PIE", + # pylint ('PL', 'PLC', 'PLE', 'PLR', 'PLW') + "PL", # flake8-pytest-style ('PT') "PT", # flake8-use-pathlib ('PTH') # NOT YET USED # flake8-pyi ('PYI') - # NOT YET USED + # Stub files are not used in Sphinx # flake8-quotes ('Q') # "Q000", # Double quotes found but single quotes preferred # "Q001", # Single quote multiline found but double quotes preferred @@ -262,13 +216,13 @@ select = [ "RET507", # Unnecessary `{branch}` after `continue` statement "RET508", # Unnecessary `{branch}` after `break` statement # flake8-raise ('RSE') - "RSE102", # Unnecessary parentheses on raised exception + "RSE", # ruff-specific rules ('RUF') # "RUF001", # String contains ambiguous {}. Did you mean {}? "RUF002", # Docstring contains ambiguous {}. Did you mean {}? # "RUF003", # Comment contains ambiguous {}. Did you mean {}? "RUF005", # Consider `{expression}` instead of concatenation - "RUF006", # Store a reference to the return value of `asyncio.{method}` + "RUF006", # Store a reference to the return value of `{expr}.{method}` "RUF007", # Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs "RUF008", # Do not use mutable default values for dataclass attributes "RUF009", # Do not perform function call `{name}` in dataclass defaults @@ -281,58 +235,24 @@ select = [ "RUF018", # Avoid assignment expressions in `assert` statements "RUF019", # Unnecessary key check before dictionary access "RUF020", # `{never_like} | T` is equivalent to `T` +# "RUF021", # Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear +# "RUF022", # `__all__` is not sorted +# "RUF023", # `{}.__slots__` is not sorted + "RUF024", # Do not pass mutable objects as values to `dict.fromkeys` + "RUF026", # `default_factory` is a positional-only argument to `defaultdict` +# "RUF027", # Possible f-string without an `f` prefix +# "RUF028", # This suppression comment is invalid because {} +# "RUF029", # Function `{name}` is declared `async`, but doesn't `await` or use `async` features. + "RUF030", # `print()` expression in `assert` statement is likely unintentional +# "RUF031", # Use parentheses for tuples in subscripts. + "RUF032", # `Decimal()` called with float literal argument + "RUF033", # `__post_init__` method with argument defaults + "RUF034", # Useless if-else condition # "RUF100", # Unused `noqa` directive + "RUF101", # `{original}` is a redirect to `{target}` "RUF200", # Failed to parse pyproject.toml: {message} # flake8-bandit ('S') -# "S101", # Use of `assert` detected - "S102", # Use of `exec` detected - "S103", # `os.chmod` setting a permissive mask `{mask:#o}` on file or directory - "S104", # Possible binding to all interfaces -# "S105", # Possible hardcoded password assigned to: "{}" - "S106", # Possible hardcoded password assigned to argument: "{}" - "S107", # Possible hardcoded password assigned to function default: "{}" - "S108", # Probable insecure usage of temporary file or directory: "{}" -# "S110", # `try`-`except`-`pass` detected, consider logging the exception - "S112", # `try`-`except`-`continue` detected, consider logging the exception -# "S113", # Probable use of requests call without timeout -# "S301", # `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue - "S302", # Deserialization with the `marshal` module is possibly dangerous - "S303", # Use of insecure MD2, MD4, MD5, or SHA1 hash function - "S304", # Use of insecure cipher, replace with a known secure cipher such as AES - "S305", # Use of insecure cipher mode, replace with a known secure cipher such as AES - "S306", # Use of insecure and deprecated function (`mktemp`) - "S307", # Use of possibly insecure function; consider using `ast.literal_eval` - "S308", # Use of `mark_safe` may expose cross-site scripting vulnerabilities - "S310", # Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes - "S312", # Telnet-related functions are being called. Telnet is considered insecure. Use SSH or some other encrypted protocol. - "S313", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S314", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S315", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S316", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S317", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S318", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S319", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "S320", # Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks - "S321", # FTP-related functions are being called. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol. - "S323", # Python allows using an insecure context via the `_create_unverified_context` that reverts to the previous behavior that does not validate certificates or perform hostname checks. -# "S324", # Probable use of insecure hash functions in `hashlib`: `{string}` - "S501", # Probable use of `{string}` call with `verify=False` disabling SSL certificate checks - "S506", # Probable use of unsafe loader `{name}` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. - "S508", # The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - "S509", # You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - "S601", # Possible shell injection via Paramiko call; check inputs are properly sanitized - "S602", # `subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell` -# "S603", # `subprocess` call: check for execution of untrusted input - "S604", # Function call with `shell=True` parameter identified, security issue - "S605", # Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell` - "S606", # Starting a process without a shell -# "S607", # Starting a process with a partial executable path - "S608", # Possible SQL injection vector through string-based query construction - "S609", # Possible wildcard injection in call due to `*` usage - "S612", # Use of insecure `logging.config.listen` detected -# "S701", # Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function. -# "S702", # Mako templates allow HTML and JavaScript rendering by default and are inherently open to XSS attacks + "S", # flake8-simplify ('SIM') "SIM", # flake8-simplify # flake8-self ('SLF') @@ -340,23 +260,21 @@ select = [ # flake8-slots ('SLOT') "SLOT", # flake8-debugger ('T10') - "T100", # Trace found: `{name}` used + "T10", # flake8-print ('T20') - "T201", # `print` found - "T203", # `pprint` found + "T20", # flake8-type-checking ('TCH') "TCH", # flake8-todos ('TD') # "TD001", # Invalid TODO tag: `{tag}` +# "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` # "TD003", # Missing issue link on the line following this TODO # "TD004", # Missing colon in TODO # "TD005", # Missing issue description after `TODO` "TD006", # Invalid TODO capitalization: `{tag}` should be `TODO` "TD007", # Missing space after colon in TODO # flake8-tidy-imports ('TID') - "TID251", # `{name}` is banned: {message} - "TID252", # Relative imports from parent modules are banned - "TID253", # `{name}` is banned at the module level + "TID", # flake8-trio ('TRIO') # Trio is not used in Sphinx # tryceratops ('TRY') @@ -390,6 +308,12 @@ select = [ # whitelist ``print`` for stdout messages "sphinx/_cli/__init__.py" = ["T201"] +# allow use of ``pickle`` +"sphinx/{application,builders/__init__,environment/__init__,ext/coverage,search/__init__,versioning}.py" = [ + "S301", + "S403", +] + # whitelist ``print`` for stdout messages "sphinx/cmd/build.py" = ["T201"] "sphinx/cmd/make_mode.py" = ["T201"] @@ -401,6 +325,9 @@ select = [ # whitelist ``print`` for stdout messages "sphinx/ext/intersphinx/_cli.py" = ["T201"] +# whitelist ``token`` in docstring parsing +"sphinx/ext/napoleon/docstring.py" = ["S105"] + # whitelist ``print`` for stdout messages "sphinx/testing/fixtures.py" = ["T201"] @@ -413,10 +340,19 @@ select = [ "sphinx/search/*" = ["E501"] +# whitelist ``token`` in date format parsing +"sphinx/util/i18n.py" = ["S105"] + +# whitelist ``token`` in literal parsing +"sphinx/writers/html5.py" = ["S105"] + "tests/*" = [ "E501", "ANN", # tests don't need annotations "D402", + "PLC1901", # whitelist comparisons to the empty string ('') + "S301", # allow use of ``pickle`` + "S403", # allow use of ``pickle`` "T201", # whitelist ``print`` for tests ] diff --git a/CHANGES.rst b/CHANGES.rst index 5ccde5de8bc..b47f417e9a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,165 +1,25 @@ -Release 8.1.0 (released Oct 10, 2024) -===================================== +Release 8.2.0 (in development) +============================== Dependencies ------------ -* #12756: Add lower-bounds to the ``sphinxcontrib-*`` dependencies. - Patch by Adam Turner. -* #12833: Update the LaTeX ``parskip`` package from 2001 to 2018. - Patch by Jean-François B. +* #13000: Drop Python 3.10 support. Incompatible changes -------------------- -* #12763: Remove unused internal class ``sphinx.util.Tee``. - Patch by Adam Turner. -* #12822: LaTeX: for Unicode engines, the :ref:`fvset` default is changed to - ``'\\fvset{fontsize=auto}'`` from ``'\\fvset{fontsize=\\small}'``. - Code-blocks are unchanged as FreeMono is now loaded with ``Scale=0.9``. - An adjustment to existing projects is needed only if they used a custom - :ref:`fontpkg` configuration and did not set :ref:`fvset`. - Patch by Jean-François B. -* #12875: Disable smartquotes for languages: ``zh_CN`` and ``zh_TW`` by default. - Patch by A. Rafey Khan. - Deprecated ---------- -* #12762: Deprecate ``sphinx.util.import_object``. - Use :py:func:`importlib.import_module` instead. - Patch by Adam Turner. -* #12766: Deprecate ``sphinx.util.FilenameUniqDict`` - and ``sphinx.util.DownloadFiles``. - Patch by Adam Turner. - Features added -------------- -* #11328: Mention evaluation of templated content during production of static - output files. - Patch by James Addison. -* #12704: LaTeX: make :dudir:`contents `, :dudir:`topic`, - and :dudir:`sidebar` directives separately customizable for PDF output. - Patch by Jean-François B. and Bénédikt Tran. -* #12474: Support type-dependent search result highlighting via CSS. - Patch by Tim Hoffmann. -* #12652: LaTeX: Add :confval:`math_numsep` support to latex builder. - Patch by Thomas Fanning and Jean-François B. -* #12743: No longer exit on the first warning when - :option:`--fail-on-warning ` is used. - Instead, exit with a non-zero status if any warnings were generated - during the build. - Patch by Adam Turner. -* #12743: Add :option:`sphinx-build --exception-on-warning`, - to raise an exception when warnings are emitted during the build. - Patch by Adam Turner and Jeremy Maitin-Shepard. -* #12907: Add :confval:`html_last_updated_use_utc` to allow using - universal time (GMT/UTC) instead of local time for the date-time - supplied to :confval:`html_last_updated_fmt`. - Patch by Adam Turner. -* #12910: Copyright entries now support the ``'%Y'`` placeholder - to substitute the current year. - This is helpful for reducing the reliance on Python modules - such as :py:mod:`time` or :py:mod:`datetime` in :file:`conf.py`. - See :ref:`the docs ` for further detail. - Patch by Adam Turner. -* #11781: Add roles for referencing CVEs (:rst:role:`:cve: `) - and CWEs (:rst:role:`:cwe: `). - Patch by Hugo van Kemenade. -* #11809: Improve the formatting for RFC section anchors. - Patch by Jakub Stasiak and Adam Turner. -* #12852: Support a :attr:`.Builder.supported_linkcode` attribute - for builders to enable use of :mod:`sphinx.ext.linkcode`-generated - references. - Patch by James Knight. -* #12949: Print configuration options that differ from the pickled environment. - This can be helpful in diagnosing the cause of a full rebuild. - Patch by Adam Turner. - Bugs fixed ---------- -* #12514: intersphinx: fix the meaning of a negative value for - :confval:`intersphinx_cache_limit`. - Patch by Shengyu Zhang. -* #12722: LaTeX: avoid TeX reporting ``Overfull \hbox`` from too long - strings in a codeline when the problem has actually been solved thanks - to :ref:`latexsphinxsetupforcewraps`. - Patch by Jean-François B. -* #12730: The ``UnreferencedFootnotesDetector`` transform has been improved - to more consistently detect unreferenced footnotes. - Note, the priority of the transform has been changed from 200 to 622, - so that it now runs after the docutils ``Footnotes`` resolution transform. - Patch by Chris Sewell. -* #12778: LaTeX: let :ref:`'sphinxsetup' ` - ``div.topic_box-shadow`` key if used with only one dimension set both - x-offset and y-offset as per documentation. - Patch by Jean-François B. -* #12587: Do not warn when potential ambiguity detected during Intersphinx - resolution occurs due to duplicate targets that differ case-insensitively. - Patch by James Addison. -* #12639: Fix singular and plural search results text. - Patch by Hugo van Kemenade. -* #12645: Correctly support custom gettext output templates. - Patch by Jeremy Bowman. -* #12717: LaTeX: let :option:`-q ` (quiet) option for - :program:`sphinx-build -M latexpdf` or :program:`make latexpdf` (``O=-q``) - get passed to :program:`latexmk`. Let :option:`-Q ` - (silent) apply as well to the PDF build phase. - Patch by Jean-François B. -* #12744: LaTeX: Classes injected by a custom interpreted text role now give - rise to nested ``\DUrole``'s, rather than a single one with comma separated - classes. - Patch by Jean-François B. -* #12831: LaTeX: avoid large voids sometimes occurring at page bottoms. - Patch by Jean-François B. -* #11970, #12551: singlehtml builder: make target URIs to be same-document - references in the sense of :rfc:`RFC 3986, §4.4 <3986#section-4.4>`, - e.g., ``index.html#foo`` becomes ``#foo``. - (note: continuation of a partial fix added in Sphinx 7.3.0) - Patch by James Addison (with reference to prior work by Eric Norige). -* #12735: Fix :pep:`695` generic classes LaTeX output formatting. - Patch by Jean-François B. and Bénédikt Tran. -* #12782: intersphinx: fix double forward slashes when generating the inventory - file URL (user-defined base URL of an intersphinx project are left untouched - even if they end with double forward slashes). - Patch by Bénédikt Tran. -* #12796: Enable parallel reading if requested, - even if there are fewer than 6 documents. - Patch by Matthias Geier. -* #12844: Restore support for ``:noindex:`` for the :rst:dir:`js:module` - and :rst:dir:`py:module` directives. - Patch by Stephen Finucane. -* #12916: Restore support for custom templates named with the legacy ``_t`` - suffix during ``apidoc`` RST rendering (regression in 7.4.0). - Patch by James Addison. -* #12451: Only substitute copyright notice years with values from - ``SOURCE_DATE_EPOCH`` for entries that match the current system clock year, - and disallow substitution of future years. - Patch by James Addison and Adam Turner. -* #12905: intersphinx: fix flipped use of :confval:`intersphinx_cache_limit`, - which always kept the cache for positive values, and always refreshed it for - negative ones. - Patch by Nico Madysa. -* #12888: Add a warning when document is included in multiple toctrees - and ensure deterministic resolution of global toctree in parallel builds - by choosing the lexicographically greatest parent document. - Patch by A. Rafey Khan -* #12995: Significantly improve performance when building the search index - for Chinese languages. - Patch by Adam Turner. -* #12767: :py:meth:`.Builder.write` is typed as ``final``, meaning that the - :event:`write-started` event may be relied upon by extensions. - A new :py:meth:`.Builder.write_documents` method has been added to - control how documents are written. - This is intended for builders that do not output a file for each document. - Patch by Adam Turner. - +* #13060: HTML Search: use ``Map`` to store per-file term scores. + Patch by James Addison Testing ------- - -* #12141: Migrate from the deprecated ``karma`` JavaScript test framework to - the actively-maintained ``jasmine`` framework. Test coverage is unaffected. - Patch by James Addison. diff --git a/doc/_static/diagrams/sphinx_build_flow.dot b/doc/_static/diagrams/sphinx_build_flow.dot index 59fbdd0d1e2..702272fcbeb 100644 --- a/doc/_static/diagrams/sphinx_build_flow.dot +++ b/doc/_static/diagrams/sphinx_build_flow.dot @@ -13,26 +13,54 @@ digraph build { shape=record label = "Sphinx | __init__ | build" ]; + "legend" [ + shape=record + label = < + + + + +
Method types
Final
Overridable
Abstract
> + ]; + {rank=same; "Sphinx" "legend" }; + + "Builder.init" [color=darkblue]; + "Builder.build_all" [color=darkorange]; + "Builder.build_specific" [color=darkorange]; + "Builder.build_update" [color=darkorange]; + "Sphinx":init -> "Builder.init"; "Sphinx":build -> "Builder.build_all"; "Sphinx":build -> "Builder.build_specific"; - "Builder.build_update" [ - shape=record - label = " Builder.build_update | Builder.get_outdated_docs" - ]; - "Sphinx":build -> "Builder.build_update":p1 ; + "Sphinx":build -> "Builder.build_update"; + + "Builder.get_outdated_docs" [color=darkgreen]; + "Builder.build_update" -> "Builder.get_outdated_docs"; + + "Builder.build" [color=darkorange]; "Builder.build_all" -> "Builder.build"; "Builder.build_specific" -> "Builder.build"; "Builder.build_update":p1 -> "Builder.build"; + "Builder.read" [color=darkorange]; + "Builder.write" [color=darkorange]; + "Builder.finish" [color=darkblue]; + "Builder.build" -> "Builder.read"; "Builder.build" -> "Builder.write"; "Builder.build" -> "Builder.finish"; + "Builder.read_doc" [color=darkorange]; + "Builder.write_doctree" [color=darkorange]; + "Builder.read" -> "Builder.read_doc"; "Builder.read_doc" -> "Builder.write_doctree"; + "Builder.prepare_writing" [color=darkblue]; + "Builder.copy_assets" [color=darkblue]; + "Builder.write_documents" [color=darkblue]; + "Builder.write":p1 -> "Builder.prepare_writing"; "Builder.write":p1 -> "Builder.copy_assets"; "Builder.write_documents" [ @@ -41,8 +69,13 @@ digraph build { ]; "Builder.write":p1 -> "Builder.write_documents"; + "Builder.write_doc" [color=darkgreen]; + "Builder.get_relative_uri" [color=darkblue]; + "Builder.write_documents":p1 -> "Builder.write_doc"; "Builder.write_doc" -> "Builder.get_relative_uri"; + "Builder.get_target_uri" [color=darkgreen]; + "Builder.get_relative_uri" -> "Builder.get_target_uri"; } diff --git a/doc/changes/8.1.rst b/doc/changes/8.1.rst new file mode 100644 index 00000000000..3a2c8cc28bb --- /dev/null +++ b/doc/changes/8.1.rst @@ -0,0 +1,205 @@ +========== +Sphinx 8.1 +========== + + +Release 8.1.3 (released Oct 13, 2024) +===================================== + +Bugs fixed +---------- + +* #13013: Restore support for :func:`!cut_lines` with no object type. + Patch by Adam Turner. + +Release 8.1.2 (released Oct 12, 2024) +===================================== + +Bugs fixed +---------- + +* #13012: Expose :exc:`sphinx.errors.ExtensionError` in ``sphinx.util`` + for backwards compatibility. + This will be removed in Sphinx 9, as exposing the exception + in ``sphinx.util`` was never intentional. + :exc:`!ExtensionError` has been part of ``sphinx.errors`` since Sphinx 0.9. + Patch by Adam Turner. + +Release 8.1.1 (released Oct 11, 2024) +===================================== + +Bugs fixed +---------- + +* #13006: Use the preferred https://www.cve.org/ URL for + the :rst:role:`:cve: ` role. + Patch by Hugo van Kemenade. +* #13007: LaTeX: Improve resiliency when the required + ``fontawesome`` or ``fontawesome5`` packages are not installed. + Patch by Jean-François B. + +Release 8.1.0 (released Oct 10, 2024) +===================================== + +Dependencies +------------ + +* #12756: Add lower-bounds to the ``sphinxcontrib-*`` dependencies. + Patch by Adam Turner. +* #12833: Update the LaTeX ``parskip`` package from 2001 to 2018. + Patch by Jean-François B. + +Incompatible changes +-------------------- + +* #12763: Remove unused internal class ``sphinx.util.Tee``. + Patch by Adam Turner. +* #12822: LaTeX: for Unicode engines, the :ref:`fvset` default is changed to + ``'\\fvset{fontsize=auto}'`` from ``'\\fvset{fontsize=\\small}'``. + Code-blocks are unchanged as FreeMono is now loaded with ``Scale=0.9``. + An adjustment to existing projects is needed only if they used a custom + :ref:`fontpkg` configuration and did not set :ref:`fvset`. + Patch by Jean-François B. +* #12875: Disable smartquotes for languages: ``zh_CN`` and ``zh_TW`` by default. + Patch by A. Rafey Khan. + +Deprecated +---------- + +* #12762: Deprecate ``sphinx.util.import_object``. + Use :py:func:`importlib.import_module` instead. + Patch by Adam Turner. +* #12766: Deprecate ``sphinx.util.FilenameUniqDict`` + and ``sphinx.util.DownloadFiles``. + Patch by Adam Turner. + +Features added +-------------- + +* #11328: Mention evaluation of templated content during production of static + output files. + Patch by James Addison. +* #12704: LaTeX: make :dudir:`contents `, :dudir:`topic`, + and :dudir:`sidebar` directives separately customizable for PDF output. + Patch by Jean-François B. and Bénédikt Tran. +* #12474: Support type-dependent search result highlighting via CSS. + Patch by Tim Hoffmann. +* #12652: LaTeX: Add :confval:`math_numsep` support to latex builder. + Patch by Thomas Fanning and Jean-François B. +* #12743: No longer exit on the first warning when + :option:`--fail-on-warning ` is used. + Instead, exit with a non-zero status if any warnings were generated + during the build. + Patch by Adam Turner. +* #12743: Add :option:`sphinx-build --exception-on-warning`, + to raise an exception when warnings are emitted during the build. + Patch by Adam Turner and Jeremy Maitin-Shepard. +* #12907: Add :confval:`html_last_updated_use_utc` to allow using + universal time (GMT/UTC) instead of local time for the date-time + supplied to :confval:`html_last_updated_fmt`. + Patch by Adam Turner. +* #12910: Copyright entries now support the ``'%Y'`` placeholder + to substitute the current year. + This is helpful for reducing the reliance on Python modules + such as :py:mod:`time` or :py:mod:`datetime` in :file:`conf.py`. + See :ref:`the docs ` for further detail. + Patch by Adam Turner. +* #11781: Add roles for referencing CVEs (:rst:role:`:cve: `) + and CWEs (:rst:role:`:cwe: `). + Patch by Hugo van Kemenade. +* #11809: Improve the formatting for RFC section anchors. + Patch by Jakub Stasiak and Adam Turner. +* #12852: Support a :attr:`.Builder.supported_linkcode` attribute + for builders to enable use of :mod:`sphinx.ext.linkcode`-generated + references. + Patch by James Knight. +* #12949: Print configuration options that differ from the pickled environment. + This can be helpful in diagnosing the cause of a full rebuild. + Patch by Adam Turner. + +Bugs fixed +---------- + +* #12514: intersphinx: fix the meaning of a negative value for + :confval:`intersphinx_cache_limit`. + Patch by Shengyu Zhang. +* #12722: LaTeX: avoid TeX reporting ``Overfull \hbox`` from too long + strings in a codeline when the problem has actually been solved thanks + to :ref:`latexsphinxsetupforcewraps`. + Patch by Jean-François B. +* #12730: The ``UnreferencedFootnotesDetector`` transform has been improved + to more consistently detect unreferenced footnotes. + Note, the priority of the transform has been changed from 200 to 622, + so that it now runs after the docutils ``Footnotes`` resolution transform. + Patch by Chris Sewell. +* #12778: LaTeX: let :ref:`'sphinxsetup' ` + ``div.topic_box-shadow`` key if used with only one dimension set both + x-offset and y-offset as per documentation. + Patch by Jean-François B. +* #12587: Do not warn when potential ambiguity detected during Intersphinx + resolution occurs due to duplicate targets that differ case-insensitively. + Patch by James Addison. +* #12639: Fix singular and plural search results text. + Patch by Hugo van Kemenade. +* #12645: Correctly support custom gettext output templates. + Patch by Jeremy Bowman. +* #12717: LaTeX: let :option:`-q ` (quiet) option for + :program:`sphinx-build -M latexpdf` or :program:`make latexpdf` (``O=-q``) + get passed to :program:`latexmk`. Let :option:`-Q ` + (silent) apply as well to the PDF build phase. + Patch by Jean-François B. +* #12744: LaTeX: Classes injected by a custom interpreted text role now give + rise to nested ``\DUrole``'s, rather than a single one with comma separated + classes. + Patch by Jean-François B. +* #12831: LaTeX: avoid large voids sometimes occurring at page bottoms. + Patch by Jean-François B. +* #11970, #12551: singlehtml builder: make target URIs to be same-document + references in the sense of :rfc:`RFC 3986, §4.4 <3986#section-4.4>`, + e.g., ``index.html#foo`` becomes ``#foo``. + (note: continuation of a partial fix added in Sphinx 7.3.0) + Patch by James Addison (with reference to prior work by Eric Norige). +* #12735: Fix :pep:`695` generic classes LaTeX output formatting. + Patch by Jean-François B. and Bénédikt Tran. +* #12782: intersphinx: fix double forward slashes when generating the inventory + file URL (user-defined base URL of an intersphinx project are left untouched + even if they end with double forward slashes). + Patch by Bénédikt Tran. +* #12796: Enable parallel reading if requested, + even if there are fewer than 6 documents. + Patch by Matthias Geier. +* #12844: Restore support for ``:noindex:`` for the :rst:dir:`js:module` + and :rst:dir:`py:module` directives. + Patch by Stephen Finucane. +* #12916: Restore support for custom templates named with the legacy ``_t`` + suffix during ``apidoc`` RST rendering (regression in 7.4.0). + Patch by James Addison. +* #12451: Only substitute copyright notice years with values from + ``SOURCE_DATE_EPOCH`` for entries that match the current system clock year, + and disallow substitution of future years. + Patch by James Addison and Adam Turner. +* #12905: intersphinx: fix flipped use of :confval:`intersphinx_cache_limit`, + which always kept the cache for positive values, and always refreshed it for + negative ones. + Patch by Nico Madysa. +* #12888: Add a warning when document is included in multiple toctrees + and ensure deterministic resolution of global toctree in parallel builds + by choosing the lexicographically greatest parent document. + Patch by A. Rafey Khan +* #12995: Significantly improve performance when building the search index + for Chinese languages. + Patch by Adam Turner. +* #12767: :py:meth:`.Builder.write` is typed as ``final``, meaning that the + :event:`write-started` event may be relied upon by extensions. + A new :py:meth:`.Builder.write_documents` method has been added to + control how documents are written. + This is intended for builders that do not output a file for each document. + Patch by Adam Turner. + + +Testing +------- + +* #12141: Migrate from the deprecated ``karma`` JavaScript test framework to + the actively-maintained ``jasmine`` framework. Test coverage is unaffected. + Patch by James Addison. diff --git a/doc/changes/index.rst b/doc/changes/index.rst index 79f3d8953dc..3542e0bb6b1 100644 --- a/doc/changes/index.rst +++ b/doc/changes/index.rst @@ -24,6 +24,7 @@ Prior releases .. toctree:: :maxdepth: 2 + 8.1 8.0 7.4 7.3 diff --git a/doc/development/html_themes/templating.rst b/doc/development/html_themes/templating.rst index e2de045f0b8..e7c1d11f453 100644 --- a/doc/development/html_themes/templating.rst +++ b/doc/development/html_themes/templating.rst @@ -308,20 +308,13 @@ in the future. .. versionadded:: 4.0 .. data:: master_doc + root_doc - Same as :data:`root_doc`. + The value of :confval:`master_doc` or :confval:`root_doc` (aliases), + for usage with :func:`pathto`. - .. versionchanged:: 4.0 - - Renamed to ``root_doc``. - -.. data:: root_doc - - The value of :confval:`root_doc`, for usage with :func:`pathto`. - - .. versionchanged:: 4.0 - - Renamed from ``master_doc``. + .. versionadded:: 4.0 + The :data:`!root_doc` template variable. .. data:: pagename diff --git a/doc/development/tutorials/examples/autodoc_intenum.py b/doc/development/tutorials/examples/autodoc_intenum.py index 7a19a2331d7..286476924f5 100644 --- a/doc/development/tutorials/examples/autodoc_intenum.py +++ b/doc/development/tutorials/examples/autodoc_intenum.py @@ -35,16 +35,15 @@ def add_directive_header(self, sig: str) -> None: def add_content( self, more_content: StringList | None, - no_docstring: bool = False, ) -> None: - super().add_content(more_content, no_docstring) + super().add_content(more_content) source_name = self.get_sourcename() enum_object: IntEnum = self.object use_hex = self.options.hex self.add_line('', source_name) - for the_member_name, enum_member in enum_object.__members__.items(): + for the_member_name, enum_member in enum_object.__members__.items(): # type: ignore[attr-defined] the_member_value = enum_member.value if use_hex: the_member_value = hex(the_member_value) diff --git a/doc/extdev/builderapi.rst b/doc/extdev/builderapi.rst index ccc0da8c5bd..5267a16a1b9 100644 --- a/doc/extdev/builderapi.rst +++ b/doc/extdev/builderapi.rst @@ -12,11 +12,11 @@ Builder API It follows this basic workflow: .. graphviz:: /_static/diagrams/sphinx_build_flow.dot - :caption: UML for the standard Sphinx build workflow + :caption: Call graph for the standard Sphinx build workflow .. rubric:: Overridable Attributes - These attributes should be set on builder sub-classes: + These class attributes should be set on builder sub-classes: .. autoattribute:: name .. autoattribute:: format @@ -29,8 +29,7 @@ Builder API .. rubric:: Core Methods - These methods are predefined and should generally not be overridden, - since they form the core of the build process: + These methods define the core build workflow and must not be overridden: .. automethod:: build_all .. automethod:: build_specific @@ -73,11 +72,12 @@ Builder API Builder sub-classes can set these attributes to support built-in extensions: .. attribute:: supported_linkcode + :type: str By default, the :mod:`linkcode ` extension will only inject references for an ``html`` builder. - The ``supported_linkcode`` attribute can be defined in a non-HTML builder - to support managing references generated by linkcode. + The ``supported_linkcode`` class attribute can be defined in a + non-HTML builder to support managing references generated by linkcode. The expected value for this attribute is an expression which is compatible with :rst:dir:`only`. diff --git a/doc/extdev/i18n.rst b/doc/extdev/i18n.rst index c5ffc848306..3c476820fbd 100644 --- a/doc/extdev/i18n.rst +++ b/doc/extdev/i18n.rst @@ -51,8 +51,8 @@ In practice, you have to: :caption: src/__init__.py def setup(app): - package_dir = path.abspath(path.dirname(__file__)) - locale_dir = os.path.join(package_dir, 'locales') + package_dir = Path(__file__).parent.resolve() + locale_dir = package_dir / 'locales' app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) #. Generate message catalog template ``*.pot`` file, usually in ``locale/`` diff --git a/doc/glossary.rst b/doc/glossary.rst index 24653b617d1..272bf0fae5a 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -75,10 +75,8 @@ Glossary For more information, refer to :doc:`/usage/extensions/index`. master document - The document that contains the root :rst:dir:`toctree` directive. - root document - Same as :term:`master document`. + The document that contains the root :rst:dir:`toctree` directive. object The basic building block of Sphinx documentation. Every "object diff --git a/doc/internals/contributing.rst b/doc/internals/contributing.rst index 0407a1a36b5..968f49e76cf 100644 --- a/doc/internals/contributing.rst +++ b/doc/internals/contributing.rst @@ -188,18 +188,18 @@ of targets and allows testing against multiple different Python environments: tox -av -* To run unit tests for a specific Python version, such as Python 3.12: +* To run unit tests for a specific Python version, such as Python 3.13: .. code-block:: shell - tox -e py312 + tox -e py313 * Arguments to :program:`pytest` can be passed via :program:`tox`, e.g., in order to run a particular test: .. code-block:: shell - tox -e py312 tests/test_module.py::test_new_feature + tox -e py313 tests/test_module.py::test_new_feature You can also test by installing dependencies in your local environment: @@ -270,12 +270,14 @@ you to preview in :file:`build/sphinx/html`. You can also build a **live version of the documentation** that you can preview in the browser. It will detect changes and reload the page any time you make -edits. To do so, run the following command: +edits. +To do so, use `sphinx-autobuild`_ to run the following command: .. code-block:: shell sphinx-autobuild ./doc ./build/sphinx/ +.. _sphinx-autobuild: https://github.com/sphinx-doc/sphinx-autobuild Translations ~~~~~~~~~~~~ diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 2b9fb7a8e3a..9768d248cce 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -569,17 +569,17 @@ See the documentation on :ref:`intl` for details. .. confval:: locale_dirs :type: :code-py:`list[str]` - :default: :code-py:`['locale']` + :default: :code-py:`['locales']` Directories in which to search for additional message catalogs (see :confval:`language`), relative to the source directory. The directories on this path are searched by the :mod:`gettext` module. Internal messages are fetched from a text domain of ``sphinx``; - so if you add the directory :file:`./locale` to this setting, + so if you add the directory :file:`./locales` to this setting, the message catalogs (compiled from ``.po`` format using :program:`msgfmt`) - must be in :file:`./locale/{language}/LC_MESSAGES/sphinx.mo`. + must be in :file:`./locales/{language}/LC_MESSAGES/sphinx.mo`. The text domain of individual documents depends on :confval:`gettext_compact`. diff --git a/doc/usage/installation.rst b/doc/usage/installation.rst index 67da3d78603..96d1594d6e6 100644 --- a/doc/usage/installation.rst +++ b/doc/usage/installation.rst @@ -152,18 +152,18 @@ Install either ``python3x-sphinx`` using :command:`port`: :: - $ sudo port install py312-sphinx + $ sudo port install py313-sphinx To set up the executable paths, use the ``port select`` command: :: - $ sudo port select --set python python312 - $ sudo port select --set sphinx py312-sphinx + $ sudo port select --set python python313 + $ sudo port select --set sphinx py313-sphinx For more information, refer to the `package overview`__. -__ https://www.macports.org/ports.php?by=library&substr=py312-sphinx +__ https://www.macports.org/ports.php?by=library&substr=py313-sphinx Windows ~~~~~~~ diff --git a/doc/usage/quickstart.rst b/doc/usage/quickstart.rst index 4c3cde892f7..062c92e966c 100644 --- a/doc/usage/quickstart.rst +++ b/doc/usage/quickstart.rst @@ -148,6 +148,17 @@ Sphinx will build HTML files. Refer to the :doc:`sphinx-build man page ` for all options that :program:`sphinx-build` supports. +You can also build a **live version of the documentation** that you can preview +in the browser. +It will detect changes and reload the page any time you make edits. +To do so, use `sphinx-autobuild`_ to run the following command: + +.. code-block:: console + + $ sphinx-autobuild source-dir output-dir + +.. _sphinx-autobuild: https://github.com/sphinx-doc/sphinx-autobuild + However, :program:`sphinx-quickstart` script creates a :file:`Makefile` and a :file:`make.bat` which make life even easier for you. These can be executed by running :command:`make` with the name of the builder. For example. diff --git a/package-lock.json b/package-lock.json index 3340f176ee8..7f582fb94cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,9 +178,9 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -191,7 +191,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -201,18 +201,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -312,6 +300,15 @@ "node": ">= 0.6" } }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -392,7 +389,7 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, "node_modules/ejs": { @@ -417,9 +414,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "engines": { "node": ">= 0.8" @@ -449,7 +446,7 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, "node_modules/etag": { @@ -462,37 +459,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -503,54 +500,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -581,6 +530,24 @@ "node": ">=10" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -728,15 +695,6 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -922,17 +880,20 @@ "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -943,6 +904,18 @@ "node": ">= 0.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -988,7 +961,7 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/negotiator": { @@ -1001,14 +974,29 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -1056,9 +1044,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "node_modules/process-nextick-args": { @@ -1081,12 +1069,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -1203,9 +1191,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -1226,16 +1214,13 @@ "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "bin": { - "mime": "cli.js" - }, "engines": { - "node": ">=4" + "node": ">= 0.8" } }, "node_modules/send/node_modules/ms": { @@ -1244,37 +1229,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/send/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -1360,6 +1324,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1475,7 +1448,7 @@ "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "engines": { "node": ">= 0.8" @@ -1662,9 +1635,9 @@ "dev": true }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "requires": { "bytes": "3.1.2", @@ -1675,21 +1648,10 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" - }, - "dependencies": { - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - } } }, "brace-expansion": { @@ -1767,6 +1729,12 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -1831,7 +1799,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, "ejs": { @@ -1850,9 +1818,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true }, "es-define-property": { @@ -1873,7 +1841,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, "etag": { @@ -1883,80 +1851,42 @@ "dev": true }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" - }, - "dependencies": { - "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - } } }, "filelist": { @@ -1988,6 +1918,21 @@ } } }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -2085,14 +2030,6 @@ "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" - }, - "dependencies": { - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - } } }, "iconv-lite": { @@ -2244,13 +2181,13 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true }, "methods": { @@ -2259,6 +2196,12 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -2292,7 +2235,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "negotiator": { @@ -2302,11 +2245,20 @@ "dev": true }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, "package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -2342,9 +2294,9 @@ } }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "process-nextick-args": { @@ -2364,12 +2316,12 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "range-parser": { @@ -2447,9 +2399,9 @@ } }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "requires": { "debug": "2.6.9", @@ -2467,10 +2419,10 @@ "statuses": "2.0.1" }, "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true }, "ms": { @@ -2478,34 +2430,19 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true } } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { @@ -2567,6 +2504,12 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -2658,7 +2601,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, "util-deprecate": { diff --git a/pyproject.toml b/pyproject.toml index e4fcfd8fb44..9c46ba94694 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,12 @@ description = "Python documentation generator" readme = "README.rst" urls.Changelog = "https://www.sphinx-doc.org/en/master/changes.html" urls.Code = "https://github.com/sphinx-doc/sphinx" +urls.Documentation = "https://www.sphinx-doc.org/" urls.Download = "https://pypi.org/project/Sphinx/" urls.Homepage = "https://www.sphinx-doc.org/" urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues" license.text = "BSD-2-Clause" -requires-python = ">=3.10" +requires-python = ">=3.11" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ @@ -30,10 +31,10 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "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 :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Sphinx", @@ -70,7 +71,6 @@ dependencies = [ "imagesize>=1.3", "requests>=2.30.0", "packaging>=23.0", - "tomli>=2; python_version < '3.11'", "colorama>=0.4.6; sys_platform == 'win32'", ] dynamic = ["version"] @@ -81,18 +81,17 @@ docs = [ ] lint = [ "flake8>=6.0", - "ruff==0.6.9", - "mypy==1.11.1", + "ruff==0.7.0", + "mypy==1.13.0", "sphinx-lint>=0.9", "types-colorama==0.4.15.20240311", "types-defusedxml==0.7.0.20240218", "types-docutils==0.21.0.20241005", "types-Pillow==10.2.0.20240822", "types-Pygments==2.18.0.20240506", - "types-requests==2.32.0.20240914", # align with requests + "types-requests==2.32.0.20241016", # align with requests "types-urllib3==1.26.25.14", - "tomli>=2", # for mypy (Python<=3.10) - "pyright==1.1.384", + "pyright==1.1.387", "pytest>=6.0", ] test = [ @@ -139,6 +138,8 @@ exclude = [ [tool.mypy] files = [ "doc/conf.py", + "doc/development/tutorials/examples/autodoc_intenum.py", + "doc/development/tutorials/examples/helloworld.py", "sphinx", "tests", "utils", @@ -201,7 +202,7 @@ exclude = [ # tests/test_writers "^utils/convert_attestations\\.py$", ] -python_version = "3.10" +python_version = "3.11" strict = true show_column_numbers = true show_error_context = true @@ -311,9 +312,9 @@ ignore_errors = true typeCheckingMode = "strict" include = [ "doc/conf.py", - "utils", + "utils", "sphinx", - "tests", + "tests", ] reportArgumentType = "none" diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 1b6d7ab92b7..6b610e28899 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -1,6 +1,6 @@ """The Sphinx documentation toolchain.""" -__version__ = '8.1.0' +__version__ = '8.2.0' __display_version__ = __version__ # used for command line version # Keep this file executable as-is in Python 3! @@ -30,18 +30,18 @@ #: #: .. versionadded:: 1.2 #: Before version 1.2, check the string ``sphinx.__version__``. -version_info = (8, 1, 0, 'final', 0) +version_info = (8, 2, 0, 'beta', 0) package_dir = os.path.abspath(os.path.dirname(__file__)) -_in_development = False +_in_development = True if _in_development: # Only import subprocess if needed import subprocess try: if ret := subprocess.run( - ['git', 'rev-parse', '--short', 'HEAD'], + ['git', 'rev-parse', '--short', 'HEAD'], # NoQA: S607 cwd=package_dir, capture_output=True, check=False, diff --git a/sphinx/_cli/util/colour.py b/sphinx/_cli/util/colour.py index 13d733595fa..bc1a610ba0e 100644 --- a/sphinx/_cli/util/colour.py +++ b/sphinx/_cli/util/colour.py @@ -35,12 +35,12 @@ def terminal_supports_colour() -> bool: def disable_colour() -> None: - global _COLOURING_DISABLED + global _COLOURING_DISABLED # NoQA: PLW0603 _COLOURING_DISABLED = True def enable_colour() -> None: - global _COLOURING_DISABLED + global _COLOURING_DISABLED # NoQA: PLW0603 _COLOURING_DISABLED = False diff --git a/sphinx/application.py b/sphinx/application.py index 872dd7a3f4f..2d650dc231f 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -316,9 +316,9 @@ def _init_i18n(self) -> None: catalog.write_mo(self.config.language, self.config.gettext_allow_fuzzy_translations) - locale_dirs: list[str | None] = list(repo.locale_dirs) + locale_dirs: list[_StrPath | None] = list(repo.locale_dirs) locale_dirs += [None] - locale_dirs += [path.join(package_dir, 'locale')] + locale_dirs += [_StrPath(package_dir, 'locale')] self.translator, has_translation = locale.init(locale_dirs, self.config.language) if has_translation or self.config.language == 'en': @@ -1586,7 +1586,7 @@ def add_env_collector(self, collector: type[EnvironmentCollector]) -> None: logger.debug('[app] adding environment collector: %r', collector) collector().enable(self) - def add_html_theme(self, name: str, theme_path: str) -> None: + def add_html_theme(self, name: str, theme_path: str | os.PathLike[str]) -> None: """Register a HTML Theme. The *name* is a name of theme, and *theme_path* is a full path to the @@ -1616,7 +1616,7 @@ def add_html_math_renderer( """ self.registry.add_html_math_renderer(name, inline_renderers, block_renderers) - def add_message_catalog(self, catalog: str, locale_dir: str) -> None: + def add_message_catalog(self, catalog: str, locale_dir: str | os.PathLike[str]) -> None: """Register a message catalog. :param catalog: The name of the catalog @@ -1673,7 +1673,7 @@ def set_html_assets_policy(self, policy: Literal['always', 'per_page']) -> None: .. versionadded: 4.1 """ - if policy not in ('always', 'per_page'): + if policy not in {'always', 'per_page'}: raise ValueError('policy %s is not supported' % policy) self.registry.html_assets_policy = policy diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 68c62d9d125..21b73f873a0 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -59,14 +59,20 @@ class Builder: Builds target formats from the reST sources. """ - #: The builder's name, for the -b command line option. - name = '' + #: The builder's name. + #: This is the value used to select builders on the command line. + name: str = '' #: The builder's output format, or '' if no document output is produced. - format = '' - #: The message emitted upon successful build completion. This can be a - #: printf-style template string with the following keys: ``outdir``, - #: ``project`` - epilog = '' + #: This is commonly the file extension, e.g. "html", + #: though any string value is accepted. + #: The builder's format string can be used by various components + #: such as :class:`.SphinxPostTransform` or extensions to determine + #: their compatibility with the builder. + format: str = '' + #: The message emitted upon successful build completion. + #: This can be a printf-style template string + #: with the following keys: ``outdir``, ``project`` + epilog: str = '' #: default translator class for the builder. This can be overridden by #: :py:meth:`~sphinx.application.Sphinx.set_translator`. @@ -74,8 +80,8 @@ class Builder: # doctree versioning method versioning_method = 'none' versioning_compare = False - #: allow parallel write_doc() calls - allow_parallel = False + #: Whether it is safe to make parallel :meth:`~.Builder.write_doc()` calls. + allow_parallel: bool = False # support translation use_message_catalog = True @@ -83,9 +89,9 @@ class Builder: #: Image files are searched in the order in which they appear here. supported_image_types: list[str] = [] #: The builder can produce output documents that may fetch external images when opened. - supported_remote_images = False + supported_remote_images: bool = False #: The file format produced by the builder allows images to be embedded using data-URIs. - supported_data_uri_images = False + supported_data_uri_images: bool = False def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: self.srcdir = app.srcdir @@ -159,7 +165,7 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str: def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str: """Return a relative URI between two source filenames. - May raise environment.NoUri if there's no way to return a sensible URI. + :raises: :exc:`!NoUri` if there's no way to return a sensible URI. """ return relative_uri( self.get_target_uri(from_), @@ -789,7 +795,16 @@ def copy_assets(self) -> None: pass def write_doc(self, docname: str, doctree: nodes.document) -> None: - """Where you actually write something to the filesystem.""" + """ + Write the output file for the document + + :param docname: the :term:`docname `. + :param doctree: defines the content to be written. + + The output filename must be determined within this method, + typically by calling :meth:`~.Builder.get_target_uri` + or :meth:`~.Builder.get_relative_uri`. + """ raise NotImplementedError def write_doc_serialized(self, docname: str, doctree: nodes.document) -> None: diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py index 52d8d9f2a83..ffab73634ae 100644 --- a/sphinx/builders/_epub_base.py +++ b/sphinx/builders/_epub_base.py @@ -24,6 +24,8 @@ from sphinx.util.osutil import copyfile, ensuredir, relpath if TYPE_CHECKING: + from pathlib import Path + from docutils.nodes import Element, Node try: @@ -487,7 +489,8 @@ def handle_page( pagename: str, addctx: dict[str, Any], templatename: str = 'page.html', - outfilename: str | None = None, + *, + outfilename: Path | None = None, event_arg: Any = None, ) -> None: """Create a rendered page. @@ -500,7 +503,9 @@ def handle_page( return self.fix_genindex(addctx['genindexentries']) addctx['doctype'] = self.doctype - super().handle_page(pagename, addctx, templatename, outfilename, event_arg) + super().handle_page( + pagename, addctx, templatename, outfilename=outfilename, event_arg=event_arg + ) def build_mimetype(self) -> None: """Write the metainfo file mimetype.""" @@ -580,7 +585,7 @@ def build_content(self) -> None: if ext not in self.media_types: # we always have JS and potentially OpenSearch files, don't # always warn about them - if ext not in ('.js', '.xml'): + if ext not in {'.js', '.xml'}: logger.warning( __('unknown mimetype for %s, ignoring'), filename, @@ -735,7 +740,8 @@ def build_navpoints(self, nodes: list[dict[str, Any]]) -> list[NavPoint]: navstack[-1].children.append(navpoint) navstack.append(navpoint) else: - raise + msg = __('node has an invalid level') + raise ValueError(msg) lastnode = node return navstack[0].children diff --git a/sphinx/builders/dirhtml.py b/sphinx/builders/dirhtml.py index 811d6f8414e..2ae1e35a7f1 100644 --- a/sphinx/builders/dirhtml.py +++ b/sphinx/builders/dirhtml.py @@ -2,12 +2,12 @@ from __future__ import annotations -from os import path +from pathlib import Path from typing import TYPE_CHECKING from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.util import logging -from sphinx.util.osutil import SEP, os_path +from sphinx.util.osutil import SEP if TYPE_CHECKING: from sphinx.application import Sphinx @@ -32,15 +32,11 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str: return docname[:-5] # up to sep return docname + SEP - def get_outfilename(self, pagename: str) -> str: - if pagename == 'index' or pagename.endswith(SEP + 'index'): - outfilename = path.join(self.outdir, os_path(pagename) + self.out_suffix) - else: - outfilename = path.join( - self.outdir, os_path(pagename), 'index' + self.out_suffix - ) - - return outfilename + def get_output_path(self, page_name: str, /) -> Path: + page_parts = page_name.split(SEP) + if page_parts[-1] == 'index': + page_parts.pop() + return Path(self.outdir, *page_parts, f'index{self.out_suffix}') def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index a38d12b60a8..eacb333fbe9 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -43,6 +43,7 @@ from sphinx.search import js_index from sphinx.theming import HTMLThemeFactory from sphinx.util import logging +from sphinx.util._pathlib import _StrPath from sphinx.util._timestamps import _format_rfc3339_microseconds from sphinx.util._uri import is_url from sphinx.util.console import bold @@ -57,7 +58,6 @@ _last_modified_time, copyfile, ensuredir, - os_path, relative_uri, ) from sphinx.writers.html import HTMLWriter @@ -138,6 +138,12 @@ class StandaloneHTMLBuilder(Builder): def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) + # Static and asset directories + self._static_dir = Path(self.outdir / '_static') + self._sources_dir = Path(self.outdir / '_sources') + self._downloads_dir = Path(self.outdir / '_downloads') + self._images_dir = Path(self.outdir / '_images') + # CSS files self._css_files: list[_CascadingStyleSheet] = [] @@ -187,23 +193,25 @@ def init(self) -> None: def create_build_info(self) -> BuildInfo: return BuildInfo(self.config, self.tags, frozenset({'html'})) - def _get_translations_js(self) -> str: - candidates = [ - path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js') - for dir in self.config.locale_dirs - ] + [ - path.join( - package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js' - ), - path.join( - sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js' - ), - ] + def _get_translations_js(self) -> Path | None: + for dir_ in self.config.locale_dirs: + js_file = Path(dir_, self.config.language, 'LC_MESSAGES', 'sphinx.js') + if js_file.is_file(): + return js_file + + js_file = Path( + package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js' + ) + if js_file.is_file(): + return js_file - for jsfile in candidates: - if path.isfile(jsfile): - return jsfile - return '' + js_file = Path( + sys.prefix, 'share', 'sphinx', 'locale', self.config.language, 'sphinx.js' + ) + if js_file.is_file(): + return js_file + + return None def _get_style_filenames(self) -> Iterator[str]: if isinstance(self.config.html_style, str): @@ -333,9 +341,9 @@ def math_renderer_name(self) -> str | None: return None def get_outdated_docs(self) -> Iterator[str]: - build_info_fname = self.outdir / '.buildinfo' + build_info_path = self.outdir / '.buildinfo' try: - build_info = BuildInfo.load(build_info_fname) + build_info = BuildInfo.load(build_info_path) except ValueError as exc: logger.warning(__('Failed to read build info file: %r'), exc) except OSError: @@ -344,10 +352,10 @@ def get_outdated_docs(self) -> Iterator[str]: else: if self.build_info != build_info: # log the mismatch and backup the old build info - build_info_backup = build_info_fname.with_name('.buildinfo.bak') + build_info_backup = build_info_path.with_name('.buildinfo.bak') try: - shutil.move(build_info_fname, build_info_backup) - self.build_info.dump(build_info_fname) + shutil.move(build_info_path, build_info_backup) + self.build_info.dump(build_info_path) except OSError: pass # ignore errors else: @@ -363,7 +371,7 @@ def get_outdated_docs(self) -> Iterator[str]: if self.templates: template_mtime = int(self.templates.newest_template_mtime() * 10**6) try: - old_mtime = _last_modified_time(build_info_fname) + old_mtime = _last_modified_time(build_info_path) except Exception: pass else: @@ -384,19 +392,19 @@ def get_outdated_docs(self) -> Iterator[str]: logger.debug('[build target] did not in env: %r', docname) yield docname continue - targetname = self.get_outfilename(docname) + target_name = self.get_output_path(docname) try: - targetmtime = _last_modified_time(targetname) - except Exception: - targetmtime = 0 + target_mtime = _last_modified_time(target_name) + except OSError: + target_mtime = 0 try: doc_mtime = _last_modified_time(self.env.doc2path(docname)) srcmtime = max(doc_mtime, template_mtime) - if srcmtime > targetmtime: + if srcmtime > target_mtime: logger.debug( - '[build target] targetname %r(%s), template(%s), docname %r(%s)', - targetname, - _format_rfc3339_microseconds(targetmtime), + '[build target] target_name %r(%s), template(%s), docname %r(%s)', + target_name, + _format_rfc3339_microseconds(target_mtime), _format_rfc3339_microseconds(template_mtime), docname, _format_rfc3339_microseconds(doc_mtime), @@ -497,10 +505,15 @@ def prepare_writing(self, docnames: Set[str]) -> None: rellinks: list[tuple[str, str, str, str]] = [] if self.use_index: rellinks.append(('genindex', _('General Index'), 'I', _('index'))) - for indexname, indexcls, _content, _collapse in self.domain_indices: + for index_name, index_cls, _content, _collapse in self.domain_indices: # if it has a short name - if indexcls.shortname: - rellinks.append((indexname, indexcls.localname, '', indexcls.shortname)) + if index_cls.shortname: + rellinks.append(( + index_name, + index_cls.localname, + '', + index_cls.shortname, + )) # add assets registered after ``Builder.init()``. for css_filename, attrs in self.app.registry.css_files: @@ -555,6 +568,10 @@ def prepare_writing(self, docnames: Set[str]) -> None: } self.globalcontext |= self.config.html_context + if self.copysource: + # Create _sources + ensuredir(self._sources_dir) + def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, Any]: """Collect items for the template context of a page.""" # find out relations @@ -705,8 +722,12 @@ def gen_additional_pages(self) -> None: # the opensearch xml file if self.config.html_use_opensearch and self.search: logger.info('opensearch ', nonl=True) - fn = path.join(self.outdir, '_static', 'opensearch.xml') - self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) + self.handle_page( + 'opensearch', + {}, + 'opensearch.xml', + outfilename=self._static_dir / 'opensearch.xml', + ) def write_genindex(self) -> None: # the total count of lines for each index letter, used to distribute @@ -739,19 +760,19 @@ def write_genindex(self) -> None: self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self) -> None: - for indexname, indexcls, content, collapse in self.domain_indices: - indexcontext = { - 'indextitle': indexcls.localname, + for index_name, index_cls, content, collapse in self.domain_indices: + index_context = { + 'indextitle': index_cls.localname, 'content': content, 'collapse_index': collapse, } - logger.info(indexname + ' ', nonl=True) - self.handle_page(indexname, indexcontext, 'domainindex.html') + logger.info('%s ', index_name, nonl=True) + self.handle_page(index_name, index_context, 'domainindex.html') def copy_image_files(self) -> None: if self.images: stringify_func = ImageAdapter(self.app.env).get_original_image_uri - ensuredir(self.outdir / self.imagedir) + ensuredir(self._images_dir) for src in status_iterator( self.images, __('copying images... '), @@ -764,7 +785,7 @@ def copy_image_files(self) -> None: try: copyfile( self.srcdir / src, - self.outdir / self.imagedir / dest, + self._images_dir / dest, force=True, ) except Exception as err: @@ -778,7 +799,7 @@ def to_relpath(f: str) -> str: # copy downloadable files if self.env.dlfiles: - ensuredir(self.outdir / '_downloads') + ensuredir(self._downloads_dir) for src in status_iterator( self.env.dlfiles, __('copying downloadable files... '), @@ -788,7 +809,7 @@ def to_relpath(f: str) -> str: stringify_func=to_relpath, ): try: - dest = self.outdir / '_downloads' / self.env.dlfiles[src][1] + dest = self._downloads_dir / self.env.dlfiles[src][1] ensuredir(dest.parent) copyfile(self.srcdir / src, dest, force=True) except OSError as err: @@ -800,22 +821,21 @@ def to_relpath(f: str) -> str: def create_pygments_style_file(self) -> None: """Create a style file for pygments.""" - pyg_path = path.join(self.outdir, '_static', 'pygments.css') + pyg_path = self._static_dir / 'pygments.css' with open(pyg_path, 'w', encoding='utf-8') as f: f.write(self.highlighter.get_stylesheet()) if self.dark_highlighter: - dark_path = path.join(self.outdir, '_static', 'pygments_dark.css') + dark_path = self._static_dir / 'pygments_dark.css' with open(dark_path, 'w', encoding='utf-8') as f: f.write(self.dark_highlighter.get_stylesheet()) def copy_translation_js(self) -> None: """Copy a JavaScript file for translations.""" - jsfile = self._get_translations_js() - if jsfile: + if js_file := self._get_translations_js(): copyfile( - jsfile, - self.outdir / '_static' / 'translations.js', + js_file, + self._static_dir / 'translations.js', force=True, ) @@ -827,14 +847,14 @@ def copy_stemmer_js(self) -> None: js_path = Path(jsfile) copyfile( js_path, - self.outdir / '_static' / js_path.name, + self._static_dir / js_path.name, force=True, ) else: if js_stemmer_rawcode := self.indexer.get_js_stemmer_rawcode(): copyfile( js_stemmer_rawcode, - self.outdir / '_static' / '_stemmer.js', + self._static_dir / '_stemmer.js', force=True, ) @@ -846,8 +866,8 @@ def onerror(filename: str, error: Exception) -> None: if self.theme: for entry in reversed(self.theme.get_theme_dirs()): copy_asset( - Path(entry) / 'static', - self.outdir / '_static', + Path(entry, 'static'), + self._static_dir, excluded=DOTFILES, context=context, renderer=self.templates, @@ -865,7 +885,7 @@ def onerror(filename: str, error: Exception) -> None: for entry in self.config.html_static_path: copy_asset( self.confdir / entry, - self.outdir / '_static', + self._static_dir, excluded=excluded, context=context, renderer=self.templates, @@ -878,7 +898,7 @@ def copy_html_logo(self) -> None: source_path = self.confdir / self.config.html_logo copyfile( source_path, - self.outdir / '_static' / source_path.name, + self._static_dir / source_path.name, force=True, ) @@ -887,14 +907,15 @@ def copy_html_favicon(self) -> None: source_path = self.confdir / self.config.html_favicon copyfile( source_path, - self.outdir / '_static' / source_path.name, + self._static_dir / source_path.name, force=True, ) def copy_static_files(self) -> None: try: with progress_message(__('copying static files'), nonl=False): - ensuredir(self.outdir / '_static') + # Ensure that the static directory exists + self._static_dir.mkdir(parents=True, exist_ok=True) # prepare context for templates context = self.globalcontext.copy() @@ -969,12 +990,12 @@ def load_indexer(self, docnames: Set[str]) -> None: assert self.indexer is not None keep = set(self.env.all_docs).difference(docnames) try: - searchindexfn = path.join(self.outdir, self.searchindex_filename) + search_index_path = self.outdir / self.searchindex_filename if self.indexer_dumps_unicode: - with open(searchindexfn, encoding='utf-8') as ft: + with open(search_index_path, encoding='utf-8') as ft: self.indexer.load(ft, self.indexer_format) else: - with open(searchindexfn, 'rb') as fb: + with open(search_index_path, 'rb') as fb: self.indexer.load(fb, self.indexer_format) except (OSError, ValueError): if keep: @@ -991,7 +1012,7 @@ def load_indexer(self, docnames: Set[str]) -> None: def index_page(self, pagename: str, doctree: nodes.document, title: str) -> None: # only index pages with title if self.indexer is not None and title: - filename = str(self.env.doc2path(pagename, base=False)) + filename = self.env.doc2path(pagename, base=False) metadata = self.env.metadata.get(pagename, {}) if 'no-search' in metadata or 'nosearch' in metadata: self.indexer.feed(pagename, filename, '', new_document('')) @@ -1003,15 +1024,18 @@ def _get_local_toctree( ) -> str: if 'includehidden' not in kwargs: kwargs['includehidden'] = False - if kwargs.get('maxdepth') == '': + if kwargs.get('maxdepth') == '': # NoQA: PLC1901 kwargs.pop('maxdepth') toctree = global_toctree_for_doc( self.env, docname, self, collapse=collapse, **kwargs ) return self.render_partial(toctree)['fragment'] - def get_outfilename(self, pagename: str) -> str: - return path.join(self.outdir, os_path(pagename) + self.out_suffix) + def get_output_path(self, page_name: str, /) -> Path: + return Path(self.outdir, page_name + self.out_suffix) + + def get_outfilename(self, pagename: str) -> _StrPath: + return _StrPath(self.get_output_path(pagename)) def add_sidebars(self, pagename: str, ctx: dict[str, Any]) -> None: def has_wildcard(pattern: str) -> bool: @@ -1049,7 +1073,8 @@ def handle_page( pagename: str, addctx: dict[str, Any], templatename: str = 'page.html', - outfilename: str | None = None, + *, + outfilename: Path | None = None, event_arg: Any = None, ) -> None: ctx = self.globalcontext.copy() @@ -1137,8 +1162,9 @@ def js_tag(js: _JavaScript | str) -> str: return f'' return f'' - uri = pathto(os.fspath(js.filename), resource=True) - if 'MathJax.js?' in os.fspath(js.filename): + js_filename_str = os.fspath(js.filename) + uri = pathto(js_filename_str, resource=True) + if 'MathJax.js?' in js_filename_str: # MathJax v2 reads a ``?config=...`` query parameter, # special case this and just skip adding the checksum. # https://docs.mathjax.org/en/v2.7-latest/configuration.html#considerations-for-using-combined-configuration-files @@ -1159,11 +1185,10 @@ def js_tag(js: _JavaScript | str) -> str: self._js_files[:] = self._orig_js_files self.update_page_context(pagename, templatename, ctx, event_arg) - newtmpl = self.app.emit_firstresult( + if new_template := self.app.emit_firstresult( 'html-page-context', pagename, templatename, ctx, event_arg - ) - if newtmpl: - templatename = newtmpl + ): + templatename = new_template # sort JS/CSS before rendering HTML try: # NoQA: SIM105 @@ -1200,22 +1225,23 @@ def js_tag(js: _JavaScript | str) -> str: ) raise ThemeError(msg) from exc - if not outfilename: - outfilename = self.get_outfilename(pagename) - # outfilename's path is in general different from self.outdir - ensuredir(path.dirname(outfilename)) + if outfilename: + output_path = Path(outfilename) + else: + output_path = self.get_output_path(pagename) + # The output path is in general different from self.outdir + ensuredir(output_path.parent) try: - with open( - outfilename, 'w', encoding=ctx['encoding'], errors='xmlcharrefreplace' - ) as f: - f.write(output) + output_path.write_text( + output, encoding=ctx['encoding'], errors='xmlcharrefreplace' + ) except OSError as err: - logger.warning(__('error writing file %s: %s'), outfilename, err) + logger.warning(__('error writing file %s: %s'), output_path, err) if self.copysource and ctx.get('sourcename'): # copy the source file for the "show source" link - source_name = path.join(self.outdir, '_sources', os_path(ctx['sourcename'])) - ensuredir(path.dirname(source_name)) - copyfile(self.env.doc2path(pagename), source_name, force=True) + source_file_path = self._sources_dir / ctx['sourcename'] + source_file_path.parent.mkdir(parents=True, exist_ok=True) + copyfile(self.env.doc2path(pagename), source_file_path, force=True) def update_page_context( self, pagename: str, templatename: str, ctx: dict[str, Any], event_arg: Any @@ -1228,7 +1254,7 @@ def handle_finish(self) -> None: @progress_message(__('dumping object inventory')) def dump_inventory(self) -> None: - InventoryFile.dump(path.join(self.outdir, INVENTORY_FILENAME), self.env, self) + InventoryFile.dump(self.outdir / INVENTORY_FILENAME, self.env, self) def dump_search_index(self) -> None: if self.indexer is None: @@ -1236,16 +1262,17 @@ def dump_search_index(self) -> None: with progress_message(__('dumping search index in %s') % self.indexer.label()): self.indexer.prune(self.env.all_docs) - searchindexfn = path.join(self.outdir, self.searchindex_filename) + search_index_path = self.outdir / self.searchindex_filename + search_index_tmp = self.outdir / f'{self.searchindex_filename}.tmp' # first write to a temporary file, so that if dumping fails, # the existing index won't be overwritten if self.indexer_dumps_unicode: - with open(searchindexfn + '.tmp', 'w', encoding='utf-8') as ft: + with open(search_index_tmp, 'w', encoding='utf-8') as ft: self.indexer.dump(ft, self.indexer_format) else: - with open(searchindexfn + '.tmp', 'wb') as fb: + with open(search_index_tmp, 'wb') as fb: self.indexer.dump(fb, self.indexer_format) - os.replace(searchindexfn + '.tmp', searchindexfn) + os.replace(search_index_tmp, search_index_path) def convert_html_css_files(app: Sphinx, config: Config) -> None: @@ -1319,43 +1346,49 @@ def validate_math_renderer(app: Sphinx) -> None: def validate_html_extra_path(app: Sphinx, config: Config) -> None: """Check html_extra_paths setting.""" - for entry in config.html_extra_path[:]: - extra_path = path.normpath(path.join(app.confdir, entry)) - if not path.exists(extra_path): + html_extra_path = [] + for entry in config.html_extra_path: + extra_path = (app.confdir / entry).resolve() + if extra_path.exists(): + if ( + app.outdir.drive == extra_path.drive + and extra_path.is_relative_to(app.outdir) + ): # fmt: skip + logger.warning( + __('html_extra_path entry %r is placed inside outdir'), entry + ) + else: + html_extra_path.append(entry) + else: logger.warning(__('html_extra_path entry %r does not exist'), entry) - config.html_extra_path.remove(entry) - elif ( - path.splitdrive(app.outdir)[0] == path.splitdrive(extra_path)[0] - and path.commonpath((app.outdir, extra_path)) == path.normpath(app.outdir) - ): # fmt: skip - logger.warning( - __('html_extra_path entry %r is placed inside outdir'), entry - ) - config.html_extra_path.remove(entry) + config.html_extra_path = html_extra_path def validate_html_static_path(app: Sphinx, config: Config) -> None: """Check html_static_paths setting.""" - for entry in config.html_static_path[:]: - static_path = path.normpath(path.join(app.confdir, entry)) - if not path.exists(static_path): + html_static_path = [] + for entry in config.html_static_path: + static_path = (app.confdir / entry).resolve() + if static_path.exists(): + if ( + app.outdir.drive == static_path.drive + and static_path.is_relative_to(app.outdir) + ): # fmt: skip + logger.warning( + __('html_static_path entry %r is placed inside outdir'), entry + ) + else: + html_static_path.append(entry) + else: logger.warning(__('html_static_path entry %r does not exist'), entry) - config.html_static_path.remove(entry) - elif ( - path.splitdrive(app.outdir)[0] == path.splitdrive(static_path)[0] - and path.commonpath((app.outdir, static_path)) == path.normpath(app.outdir) - ): # fmt: skip - logger.warning( - __('html_static_path entry %r is placed inside outdir'), entry - ) - config.html_static_path.remove(entry) + config.html_static_path = html_static_path def validate_html_logo(app: Sphinx, config: Config) -> None: """Check html_logo setting.""" if ( config.html_logo - and not path.isfile(path.join(app.confdir, config.html_logo)) + and not (app.confdir / config.html_logo).is_file() and not is_url(config.html_logo) ): logger.warning(__('logo file %r does not exist'), config.html_logo) @@ -1366,7 +1399,7 @@ def validate_html_favicon(app: Sphinx, config: Config) -> None: """Check html_favicon setting.""" if ( config.html_favicon - and not path.isfile(path.join(app.confdir, config.html_favicon)) + and not (app.confdir / config.html_favicon).is_file() and not is_url(config.html_favicon) ): logger.warning(__('favicon file %r does not exist'), config.html_favicon) diff --git a/sphinx/builders/html/_assets.py b/sphinx/builders/html/_assets.py index b2b3b9c2bb3..85e5e2dc27b 100644 --- a/sphinx/builders/html/_assets.py +++ b/sphinx/builders/html/_assets.py @@ -3,6 +3,7 @@ import os import warnings import zlib +from functools import cache from typing import TYPE_CHECKING, Any, NoReturn from sphinx.deprecation import RemovedInSphinx90Warning @@ -172,9 +173,14 @@ def _file_checksum(outdir: Path, filename: str | os.PathLike[str]) -> str: if '?' in filename: msg = f'Local asset file paths must not contain query strings: {filename!r}' raise ThemeError(msg) + return _file_checksum_inner(outdir.joinpath(filename).resolve()) + + +@cache +def _file_checksum_inner(file: Path) -> str: try: # Remove all carriage returns to avoid checksum differences - content = outdir.joinpath(filename).read_bytes().translate(None, b'\r') + content = file.read_bytes().translate(None, b'\r') except FileNotFoundError: return '' if not content: diff --git a/sphinx/builders/html/_build_info.py b/sphinx/builders/html/_build_info.py index 919b177a4b9..cff749c6763 100644 --- a/sphinx/builders/html/_build_info.py +++ b/sphinx/builders/html/_build_info.py @@ -65,6 +65,9 @@ def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override] self.config_hash == other.config_hash and self.tags_hash == other.tags_hash ) + def __hash__(self) -> int: + return hash((self.config_hash, self.tags_hash)) + def dump(self, filename: Path, /) -> None: build_info = ( '# Sphinx build info version 1\n' diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py index 424f933fc2a..3f2be39f128 100644 --- a/sphinx/builders/html/transforms.py +++ b/sphinx/builders/html/transforms.py @@ -73,7 +73,7 @@ def run(self, **kwargs: Any) -> None: pass def is_multiwords_key(self, parts: list[str]) -> bool: - if len(parts) >= 3 and parts[1].strip() == '': + if len(parts) >= 3 and not parts[1].strip(): name = parts[0].lower(), parts[2].lower() return name in self.multiwords_keys else: diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index c51fa7607a2..db7adc5b0d7 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -249,7 +249,7 @@ def init_multilingual(self) -> None: ) else: self.context['textgreek'] = '' - if self.context['substitutefont'] == '': + if not self.context['substitutefont']: self.context['fontsubstitution'] = '' # 'babel' key is public and user setting must be obeyed @@ -555,7 +555,7 @@ def validate_latex_theme_options(app: Sphinx, config: Config) -> None: def install_packages_for_ja(app: Sphinx) -> None: """Install packages for Japanese.""" - if app.config.language == 'ja' and app.config.latex_engine in ('platex', 'uplatex'): + if app.config.language == 'ja' and app.config.latex_engine in {'platex', 'uplatex'}: app.add_latex_package('pxjahyper', after_hyperref=True) diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py index a6258abeba1..50f03652fc8 100644 --- a/sphinx/builders/latex/theming.py +++ b/sphinx/builders/latex/theming.py @@ -55,7 +55,7 @@ def __init__(self, name: str, config: Config) -> None: else: self.docclass = config.latex_docclass.get('manual', 'report') - if name in ('manual', 'howto'): + if name in {'manual', 'howto'}: self.wrapperclass = 'sphinx' + name else: self.wrapperclass = name diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 8a4d6b1d050..57864208e17 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -566,7 +566,7 @@ class MathReferenceTransform(SphinxPostTransform): def run(self, **kwargs: Any) -> None: equations = self.env.domains.math_domain.data['objects'] for node in self.document.findall(addnodes.pending_xref): - if node['refdomain'] == 'math' and node['reftype'] in ('eq', 'numref'): + if node['refdomain'] == 'math' and node['reftype'] in {'eq', 'numref'}: docname, _ = equations.get(node['reftarget'], (None, None)) if docname: refnode = math_reference( diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 0ee0addd7b8..fcf994e8e03 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -7,6 +7,7 @@ import re import socket import time +from enum import StrEnum from html.parser import HTMLParser from os import path from queue import PriorityQueue, Queue @@ -29,7 +30,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator - from typing import Any + from typing import Any, Literal, TypeAlias from requests import Response @@ -38,6 +39,20 @@ from sphinx.util._pathlib import _StrPath from sphinx.util.typing import ExtensionMetadata + _URIProperties: TypeAlias = tuple['_Status', str, int] + + +class _Status(StrEnum): + BROKEN = 'broken' + IGNORED = 'ignored' + RATE_LIMITED = 'rate-limited' + REDIRECTED = 'redirected' + TIMEOUT = 'timeout' + UNCHECKED = 'unchecked' + UNKNOWN = 'unknown' + WORKING = 'working' + + logger = logging.getLogger(__name__) # matches to foo:// and // (a protocol relative URL) @@ -85,7 +100,7 @@ def finish(self) -> None: def process_result(self, result: CheckResult) -> None: filename = self.env.doc2path(result.docname, False) - linkstat: dict[str, str | int] = { + linkstat: dict[str, str | int | _Status] = { 'filename': str(filename), 'lineno': result.lineno, 'status': result.status, @@ -95,101 +110,91 @@ def process_result(self, result: CheckResult) -> None: } self.write_linkstat(linkstat) - if result.status == 'unchecked': - return - if result.status == 'working' and result.message == 'old': - return - if result.lineno: + if result.lineno and result.status != _Status.UNCHECKED: + # unchecked links are not logged logger.info('(%16s: line %4d) ', result.docname, result.lineno, nonl=True) - if result.status == 'ignored': - if result.message: - logger.info(darkgray('-ignored- ') + result.uri + ': ' + result.message) - else: - logger.info(darkgray('-ignored- ') + result.uri) - elif result.status == 'local': - logger.info(darkgray('-local- ') + result.uri) - self.write_entry( - 'local', result.docname, filename, result.lineno, result.uri - ) - elif result.status == 'working': - logger.info(darkgreen('ok ') + result.uri + result.message) - elif result.status == 'timeout': - if self.app.quiet: - logger.warning( - 'timeout ' + result.uri + result.message, - location=(result.docname, result.lineno), - ) - else: - logger.info( - red('timeout ') + result.uri + red(' - ' + result.message) - ) - self.write_entry( - 'timeout', - result.docname, - filename, - result.lineno, - result.uri + ': ' + result.message, - ) - self.timed_out_hyperlinks += 1 - elif result.status == 'broken': - if self.app.quiet: - logger.warning( - __('broken link: %s (%s)'), - result.uri, - result.message, - location=(result.docname, result.lineno), - ) - else: - logger.info( - red('broken ') + result.uri + red(' - ' + result.message) + + match result.status: + case _Status.RATE_LIMITED | _Status.UNCHECKED: + pass + case _Status.IGNORED: + if result.message: + msg = f'{result.uri}: {result.message}' + else: + msg = result.uri + logger.info(darkgray('-ignored- ') + msg) + case _Status.WORKING: + logger.info(darkgreen('ok ') + f'{result.uri}{result.message}') + case _Status.TIMEOUT: + if self.app.quiet: + msg = 'timeout ' + f'{result.uri}{result.message}' + logger.warning(msg, location=(result.docname, result.lineno)) + else: + msg = red('timeout ') + result.uri + red(f' - {result.message}') + logger.info(msg) + self.write_entry( + _Status.TIMEOUT, + result.docname, + filename, + result.lineno, + f'{result.uri}: {result.message}', ) - self.write_entry( - 'broken', - result.docname, - filename, - result.lineno, - result.uri + ': ' + result.message, - ) - self.broken_hyperlinks += 1 - elif result.status == 'redirected': - try: - text, color = { - 301: ('permanently', purple), - 302: ('with Found', purple), - 303: ('with See Other', purple), - 307: ('temporarily', turquoise), - 308: ('permanently', purple), - }[result.code] - except KeyError: - text, color = ('with unknown code', purple) - linkstat['text'] = text - if self.config.linkcheck_allowed_redirects: - logger.warning( - 'redirect ' + result.uri + ' - ' + text + ' to ' + result.message, - location=(result.docname, result.lineno), + self.timed_out_hyperlinks += 1 + case _Status.BROKEN: + if self.app.quiet: + logger.warning( + __('broken link: %s (%s)'), + result.uri, + result.message, + location=(result.docname, result.lineno), + ) + else: + msg = red('broken ') + result.uri + red(f' - {result.message}') + logger.info(msg) + self.write_entry( + _Status.BROKEN, + result.docname, + filename, + result.lineno, + f'{result.uri}: {result.message}', ) - else: - logger.info( - color('redirect ') - + result.uri - + color(' - ' + text + ' to ' + result.message) + self.broken_hyperlinks += 1 + case _Status.REDIRECTED: + try: + text, color = { + 301: ('permanently', purple), + 302: ('with Found', purple), + 303: ('with See Other', purple), + 307: ('temporarily', turquoise), + 308: ('permanently', purple), + }[result.code] + except KeyError: + text, color = ('with unknown code', purple) + linkstat['text'] = text + redirection = f'{text} to {result.message}' + if self.config.linkcheck_allowed_redirects: + msg = f'redirect {result.uri} - {redirection}' + logger.warning(msg, location=(result.docname, result.lineno)) + else: + msg = color('redirect ') + result.uri + color(' - ' + redirection) + logger.info(msg) + self.write_entry( + f'redirected {text}', + result.docname, + filename, + result.lineno, + f'{result.uri} to {result.message}', ) - self.write_entry( - 'redirected ' + text, - result.docname, - filename, - result.lineno, - result.uri + ' to ' + result.message, - ) - else: - raise ValueError('Unknown status %s.' % result.status) + case _Status.UNKNOWN: + msg = 'Unknown status.' + raise ValueError(msg) - def write_linkstat(self, data: dict[str, str | int]) -> None: + def write_linkstat(self, data: dict[str, str | int | _Status]) -> None: self.json_outfile.write(json.dumps(data)) self.json_outfile.write('\n') def write_entry( - self, what: str, docname: str, filename: _StrPath, line: int, uri: str + self, what: _Status | str, docname: str, filename: _StrPath, line: int, uri: str ) -> None: self.txt_outfile.write(f'{filename}:{line}: [{what}] {uri}\n') @@ -291,7 +296,12 @@ def check(self, hyperlinks: dict[str, Hyperlink]) -> Iterator[CheckResult]: for hyperlink in hyperlinks.values(): if self.is_ignored_uri(hyperlink.uri): yield CheckResult( - hyperlink.uri, hyperlink.docname, hyperlink.lineno, 'ignored', '', 0 + uri=hyperlink.uri, + docname=hyperlink.docname, + lineno=hyperlink.lineno, + status=_Status.IGNORED, + message='', + code=0, ) else: self.wqueue.put(CheckRequest(CHECK_IMMEDIATELY, hyperlink), False) @@ -330,7 +340,7 @@ class CheckResult(NamedTuple): uri: str docname: str lineno: int - status: str + status: _Status message: str code: int @@ -373,10 +383,11 @@ def __init__( self.retries: int = config.linkcheck_retries self.rate_limit_timeout = config.linkcheck_rate_limit_timeout self._allow_unauthorized = config.linkcheck_allow_unauthorized + self._timeout_status: Literal[_Status.BROKEN, _Status.TIMEOUT] if config.linkcheck_report_timeouts_as_broken: - self._timeout_status = 'broken' + self._timeout_status = _Status.BROKEN else: - self._timeout_status = 'timeout' + self._timeout_status = _Status.TIMEOUT self.user_agent = config.user_agent self.tls_verify = config.tls_verify @@ -413,7 +424,7 @@ def run(self) -> None: self.wqueue.task_done() continue status, info, code = self._check(docname, uri, hyperlink) - if status == 'rate-limited': + if status == _Status.RATE_LIMITED: logger.info( darkgray('-rate limited- ') + uri + darkgray(' | sleeping...') ) @@ -421,9 +432,7 @@ def run(self) -> None: self.rqueue.put(CheckResult(uri, docname, lineno, status, info, code)) self.wqueue.task_done() - def _check( - self, docname: str, uri: str, hyperlink: Hyperlink - ) -> tuple[str, str, int]: + def _check(self, docname: str, uri: str, hyperlink: Hyperlink) -> _URIProperties: # check for various conditions without bothering the network for doc_matcher in self.documents_exclude: @@ -432,25 +441,26 @@ def _check( f'{docname} matched {doc_matcher.pattern} from ' 'linkcheck_exclude_documents' ) - return 'ignored', info, 0 + return _Status.IGNORED, info, 0 if len(uri) == 0 or uri.startswith(('#', 'mailto:', 'tel:')): - return 'unchecked', '', 0 + return _Status.UNCHECKED, '', 0 if not uri.startswith(('http:', 'https:')): if uri_re.match(uri): # Non-supported URI schemes (ex. ftp) - return 'unchecked', '', 0 + return _Status.UNCHECKED, '', 0 src_dir = path.dirname(hyperlink.docpath) if path.exists(path.join(src_dir, uri)): - return 'working', '', 0 - return 'broken', '', 0 + return _Status.WORKING, '', 0 + return _Status.BROKEN, '', 0 # need to actually check the URI - status, info, code = '', '', 0 + status: _Status + status, info, code = _Status.UNKNOWN, '', 0 for _ in range(self.retries): status, info, code = self._check_uri(uri, hyperlink) - if status != 'broken': + if status != _Status.BROKEN: break return status, info, code @@ -464,7 +474,7 @@ def _retrieval_methods( yield self._session.head, {'allow_redirects': True} yield self._session.get, {'stream': True} - def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]: + def _check_uri(self, uri: str, hyperlink: Hyperlink) -> _URIProperties: req_url, delimiter, anchor = uri.partition('#') if delimiter and anchor: for rex in self.anchors_ignore: @@ -519,10 +529,14 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]: try: found = contains_anchor(response, anchor) except UnicodeDecodeError: - return 'ignored', 'unable to decode response content', 0 + return ( + _Status.IGNORED, + 'unable to decode response content', + 0, + ) if not found: return ( - 'broken', + _Status.BROKEN, __("Anchor '%s' not found") % quote(anchor), 0, ) @@ -543,7 +557,7 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]: except SSLError as err: # SSL failure; report that the link is broken. - return 'broken', str(err), 0 + return _Status.BROKEN, str(err), 0 except (ConnectionError, TooManyRedirects) as err: # Servers drop the connection on HEAD requests, causing @@ -556,19 +570,21 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]: # Unauthorized: the client did not provide required credentials if status_code == 401: - status = 'working' if self._allow_unauthorized else 'broken' - return status, 'unauthorized', 0 + if self._allow_unauthorized: + return _Status.WORKING, 'unauthorized', 0 + else: + return _Status.BROKEN, 'unauthorized', 0 # Rate limiting; back-off if allowed, or report failure otherwise if status_code == 429: if next_check := self.limit_rate(response_url, retry_after): self.wqueue.put(CheckRequest(next_check, hyperlink), False) - return 'rate-limited', '', 0 - return 'broken', error_message, 0 + return _Status.RATE_LIMITED, '', 0 + return _Status.BROKEN, error_message, 0 # Don't claim success/failure during server-side outages if status_code == 503: - return 'ignored', 'service unavailable', 0 + return _Status.IGNORED, 'service unavailable', 0 # For most HTTP failures, continue attempting alternate retrieval methods continue @@ -576,12 +592,12 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]: except Exception as err: # Unhandled exception (intermittent or permanent); report that # the link is broken. - return 'broken', str(err), 0 + return _Status.BROKEN, str(err), 0 else: # All available retrieval methods have been exhausted; report # that the link is broken. - return 'broken', error_message, 0 + return _Status.BROKEN, error_message, 0 # Success; clear rate limits for the origin netloc = urlsplit(req_url).netloc @@ -591,11 +607,11 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]: (response_url.rstrip('/') == req_url.rstrip('/')) or _allowed_redirect(req_url, response_url, self.allowed_redirects) ): # fmt: skip - return 'working', '', 0 + return _Status.WORKING, '', 0 elif redirect_status_code is not None: - return 'redirected', response_url, redirect_status_code + return _Status.REDIRECTED, response_url, redirect_status_code else: - return 'redirected', response_url, 0 + return _Status.REDIRECTED, response_url, 0 def limit_rate(self, response_url: str, retry_after: str | None) -> float | None: delay = DEFAULT_DELAY @@ -681,7 +697,7 @@ def __init__(self, search_anchor: str) -> None: def handle_starttag(self, tag: Any, attrs: Any) -> None: for key, value in attrs: - if key in ('id', 'name') and value == self.search_anchor: + if key in {'id', 'name'} and value == self.search_anchor: self.found = True break diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index 6715142ba91..2a6991f1d68 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -2,7 +2,6 @@ from __future__ import annotations -from os import path from typing import TYPE_CHECKING, Any from docutils import nodes @@ -74,7 +73,7 @@ def _get_local_toctree( kwargs['includehidden'] = False elif includehidden.lower() == 'true': kwargs['includehidden'] = True - if kwargs.get('maxdepth') == '': + if kwargs.get('maxdepth') == '': # NoQA: PLC1901 kwargs.pop('maxdepth') toctree = global_toctree_for_doc( self.env, docname, self, collapse=collapse, **kwargs @@ -194,8 +193,12 @@ def write_additional_files(self) -> None: if self.config.html_use_opensearch: logger.info(' opensearch', nonl=True) - fn = path.join(self.outdir, '_static', 'opensearch.xml') - self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) + self.handle_page( + 'opensearch', + {}, + 'opensearch.xml', + outfilename=self._static_dir / 'opensearch.xml', + ) def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py index b590c423e85..ac22ba99822 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -12,6 +12,7 @@ import os import subprocess import sys +from contextlib import chdir from os import path from typing import TYPE_CHECKING @@ -20,11 +21,6 @@ from sphinx.util.console import blue, bold, color_terminal, nocolor from sphinx.util.osutil import rmtree -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - from sphinx.util.osutil import _chdir as chdir - if TYPE_CHECKING: from collections.abc import Sequence @@ -110,7 +106,7 @@ def build_latexpdf(self) -> int: try: with chdir(self.build_dir_join('latex')): if '-Q' in self.opts: - with open('__LATEXSTDOUT__', 'w') as outfile: + with open('__LATEXSTDOUT__', 'w', encoding='utf-8') as outfile: returncode = subprocess.call( [ makecmd, diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index cbc9aafa673..1176dc14b40 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -96,8 +96,8 @@ def is_path(x: str) -> str: def is_path_or_empty(x: str) -> str: - if x == '': - return x + if not x: + return '' return is_path(x) @@ -121,9 +121,9 @@ def val(x: str) -> str: def boolean(x: str) -> bool: - if x.upper() not in ('Y', 'YES', 'N', 'NO'): + if x.upper() not in {'Y', 'YES', 'N', 'NO'}: raise ValidationError(__("Please enter either 'y' or 'n'.")) - return x.upper() in ('Y', 'YES') + return x.upper() in {'Y', 'YES'} def suffix(x: str) -> str: diff --git a/sphinx/config.py b/sphinx/config.py index 134a6e39467..8700ed30054 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -2,11 +2,11 @@ from __future__ import annotations -import sys import time import traceback import types import warnings +from contextlib import chdir from os import getenv, path from typing import TYPE_CHECKING, Any, Literal, NamedTuple @@ -16,11 +16,6 @@ from sphinx.util import logging from sphinx.util.osutil import fs_encoding -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - from sphinx.util.osutil import _chdir as chdir - if TYPE_CHECKING: import os from collections.abc import Collection, Iterable, Iterator, Sequence, Set diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index e0df09d2b5f..b68f6e1a729 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -13,9 +13,12 @@ from sphinx.locale import __ from sphinx.util import logging from sphinx.util._lines import parse_line_num_spec +from sphinx.util._pathlib import _StrPath from sphinx.util.docutils import SphinxDirective if TYPE_CHECKING: + import os + from docutils.nodes import Element, Node from sphinx.application import Sphinx @@ -197,8 +200,10 @@ class LiteralIncludeReader: ('diff', 'end-at'), ] - def __init__(self, filename: str, options: dict[str, Any], config: Config) -> None: - self.filename = filename + def __init__( + self, filename: str | os.PathLike[str], options: dict[str, Any], config: Config + ) -> None: + self.filename = _StrPath(filename) self.options = options self.encoding = options.get('encoding', config.source_encoding) self.lineno_start = self.options.get('lineno-start', 1) @@ -212,8 +217,9 @@ def parse_options(self) -> None: raise ValueError(msg) def read_file( - self, filename: str, location: tuple[str, int] | None = None + self, filename: str | os.PathLike[str], location: tuple[str, int] | None = None ) -> list[str]: + filename = _StrPath(filename) try: with open(filename, encoding=self.encoding, errors='strict') as f: text = f.read() @@ -222,11 +228,11 @@ def read_file( return text.splitlines(True) except OSError as exc: - msg = __('Include file %r not found or reading it failed') % filename + msg = __("Include file '%s' not found or reading it failed") % filename raise OSError(msg) from exc except UnicodeError as exc: msg = __( - 'Encoding %r used for reading included file %r seems to ' + "Encoding %r used for reading included file '%s' seems to " 'be wrong, try giving an :encoding: option' ) % (self.encoding, filename) raise UnicodeError(msg) from exc diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 3503f2e3411..85cff2e6407 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -166,7 +166,7 @@ def add_target(self, ret: list[Node]) -> None: node = cast(nodes.math_block, ret[0]) # assign label automatically if math_number_all enabled - if node['label'] == '' or (self.config.math_number_all and not node['label']): + if node['label'] == '' or (self.config.math_number_all and not node['label']): # NoQA: PLC1901 seq = self.env.new_serialno('sphinx.ext.math#equations') node['label'] = f'{self.env.docname}:{seq}' diff --git a/sphinx/domains/_domains_container.py b/sphinx/domains/_domains_container.py index 5353bcfd650..b8e389c79e4 100644 --- a/sphinx/domains/_domains_container.py +++ b/sphinx/domains/_domains_container.py @@ -4,10 +4,9 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping, Set - from typing import Any, Final, Literal, NoReturn + from typing import Any, Final, Literal, NoReturn, Self from docutils import nodes - from typing_extensions import Self from sphinx.domains import Domain from sphinx.domains.c import CDomain @@ -188,6 +187,9 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self._domain_instances == other._domain_instances + def __hash__(self) -> int: + return hash(sorted(self._domain_instances.items())) + def __setattr__(self, key: str, value: object) -> None: if key in self._core_domains: msg = f'{self.__class__.__name__!r} object does not support assignment to {key!r}' diff --git a/sphinx/domains/c/__init__.py b/sphinx/domains/c/__init__.py index b6ae757e435..e82912c167e 100644 --- a/sphinx/domains/c/__init__.py +++ b/sphinx/domains/c/__init__.py @@ -259,8 +259,14 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: self.env.temp_data['c:last_symbol'] = e.symbol msg = __("Duplicate C declaration, also defined at %s:%s.\n" "Declaration is '.. c:%s:: %s'.") - msg = msg % (e.symbol.docname, e.symbol.line, self.display_object_type, sig) - logger.warning(msg, location=signode) + logger.warning( + msg, + e.symbol.docname, + e.symbol.line, + self.display_object_type, + sig, + location=signode, + ) if ast.objectType == 'enumerator': self._add_enumerator_to_parent(ast) @@ -290,7 +296,7 @@ class CMemberObject(CObject): @property def display_object_type(self) -> str: # the distinction between var and member is only cosmetic - assert self.objtype in ('member', 'var') + assert self.objtype in {'member', 'var'} return self.objtype @@ -354,7 +360,7 @@ class CNamespaceObject(SphinxDirective): def run(self) -> list[Node]: rootSymbol = self.env.domaindata['c']['root_symbol'] - if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + if self.arguments[0].strip() in {'NULL', '0', 'nullptr'}: symbol = rootSymbol stack: list[Symbol] = [] else: @@ -383,7 +389,7 @@ class CNamespacePushObject(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + if self.arguments[0].strip() in {'NULL', '0', 'nullptr'}: return [] parser = DefinitionParser(self.arguments[0], location=self.get_location(), diff --git a/sphinx/domains/c/_ast.py b/sphinx/domains/c/_ast.py index 5147a4598f8..9fedbed5a8c 100644 --- a/sphinx/domains/c/_ast.py +++ b/sphinx/domains/c/_ast.py @@ -54,6 +54,9 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self.name == other.name + def __hash__(self) -> int: + return hash((self.name, self.is_anonymous)) + def is_anon(self) -> bool: return self.is_anonymous @@ -148,7 +151,7 @@ def describe_signature(self, signode: TextElement, mode: str, assert not self.rooted, str(self) assert len(self.names) == 1 self.names[0].describe_signature(signode, 'noneIsName', env, '', symbol) - elif mode in ('markType', 'lastIsName', 'markName'): + elif mode in {'markType', 'lastIsName', 'markName'}: # Each element should be a pending xref targeting the complete # prefix. prefix = '' @@ -175,7 +178,7 @@ def describe_signature(self, signode: TextElement, mode: str, prefix += '.' first = False txt_ident = str(ident) - if txt_ident != '': + if txt_ident: ident.describe_signature(dest, 'markType', env, prefix, symbol) prefix += txt_ident if mode == 'lastIsName': @@ -1771,6 +1774,16 @@ def __eq__(self, other: object) -> bool: and self.enumeratorScopedSymbol == other.enumeratorScopedSymbol ) + def __hash__(self) -> int: + return hash(( + self.objectType, + self.directiveType, + self.declaration, + self.semicolon, + self.symbol, + self.enumeratorScopedSymbol, + )) + def clone(self) -> ASTDeclaration: return ASTDeclaration(self.objectType, self.directiveType, self.declaration.clone(), self.semicolon) diff --git a/sphinx/domains/c/_parser.py b/sphinx/domains/c/_parser.py index 1d29c60832e..2eeb4e7ef47 100644 --- a/sphinx/domains/c/_parser.py +++ b/sphinx/domains/c/_parser.py @@ -608,7 +608,7 @@ def _parse_decl_specs_simple( if self.skip_word('register'): storage = 'register' continue - if outer in ('member', 'function'): + if outer in {'member', 'function'}: if self.skip_word('static'): storage = 'static' continue @@ -649,7 +649,7 @@ def _parse_decl_specs_simple( def _parse_decl_specs(self, outer: str | None, typed: bool = True) -> ASTDeclSpecs: if outer: - if outer not in ('type', 'member', 'function'): + if outer not in {'type', 'member', 'function'}: raise Exception('Internal error, unknown outer "%s".' % outer) leftSpecs = self._parse_decl_specs_simple(outer, typed) rightSpecs = None @@ -664,7 +664,7 @@ def _parse_decl_specs(self, outer: str | None, typed: bool = True) -> ASTDeclSpe def _parse_declarator_name_suffix( self, named: bool | str, paramMode: str, typed: bool, ) -> ASTDeclarator: - assert named in (True, False, 'single') + assert named in {True, False, 'single'} # now we should parse the name, and then suffixes if named == 'single': if self.match(identifier_re): @@ -747,7 +747,7 @@ def parser() -> ASTExpression: def _parse_declarator(self, named: bool | str, paramMode: str, typed: bool = True) -> ASTDeclarator: # 'typed' here means 'parse return type stuff' - if paramMode not in ('type', 'function'): + if paramMode not in {'type', 'function'}: raise Exception( "Internal error, unknown paramMode '%s'." % paramMode) prevErrors = [] @@ -860,7 +860,7 @@ def _parse_type(self, named: bool | str, outer: str | None = None) -> ASTType: doesn't need to name the arguments, but otherwise is a single name """ if outer: # always named - if outer not in ('type', 'member', 'function'): + if outer not in {'type', 'member', 'function'}: raise Exception('Internal error, unknown outer "%s".' % outer) assert named @@ -915,7 +915,7 @@ def _parse_type(self, named: bool | str, outer: str | None = None) -> ASTType: def _parse_type_with_init(self, named: bool | str, outer: str | None) -> ASTTypeWithInit: if outer: - assert outer in ('type', 'member', 'function') + assert outer in {'type', 'member', 'function'} type = self._parse_type(outer=outer, named=named) init = self._parse_initializer(outer=outer) return ASTTypeWithInit(type, init) @@ -987,11 +987,11 @@ def parser() -> ASTExpression: return ASTEnumerator(name, init, attrs) def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclaration: - if objectType not in ('function', 'member', - 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): + if objectType not in {'function', 'member', + 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'}: raise Exception('Internal error, unknown objectType "%s".' % objectType) - if directiveType not in ('function', 'member', 'var', - 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): + if directiveType not in {'function', 'member', 'var', + 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'}: raise Exception('Internal error, unknown directiveType "%s".' % directiveType) declaration: DeclarationType | None = None diff --git a/sphinx/domains/c/_symbol.py b/sphinx/domains/c/_symbol.py index c70b5131610..d98a57065ca 100644 --- a/sphinx/domains/c/_symbol.py +++ b/sphinx/domains/c/_symbol.py @@ -12,8 +12,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator, Sequence - - from typing_extensions import Self + from typing import Self from sphinx.environment import BuildEnvironment @@ -136,7 +135,8 @@ def _add_child(self, child: Symbol) -> None: def _remove_child(self, child: Symbol) -> None: name = child.ident.name self._children_by_name.pop(name, None) - self._children_by_docname.get(child.docname, {}).pop(name, None) + if children := self._children_by_docname.get(child.docname): + children.pop(name, None) if child.ident.is_anonymous: self._anon_children.discard(child) @@ -509,9 +509,14 @@ def merge_with(self, other: Symbol, docnames: list[str], name = str(ourChild.declaration) msg = __("Duplicate C declaration, also defined at %s:%s.\n" "Declaration is '.. c:%s:: %s'.") - msg = msg % (ourChild.docname, ourChild.line, - ourChild.declaration.directiveType, name) - logger.warning(msg, location=(otherChild.docname, otherChild.line)) + logger.warning( + msg, + ourChild.docname, + ourChild.line, + ourChild.declaration.directiveType, + name, + location=(otherChild.docname, otherChild.line), + ) else: # Both have declarations, and in the same docname. # This can apparently happen, it should be safe to diff --git a/sphinx/domains/cpp/__init__.py b/sphinx/domains/cpp/__init__.py index a043958c857..45183611106 100644 --- a/sphinx/domains/cpp/__init__.py +++ b/sphinx/domains/cpp/__init__.py @@ -359,9 +359,14 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: self.env.temp_data['cpp:last_symbol'] = e.symbol msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" "Declaration is '.. cpp:%s:: %s'.") - msg = msg % (e.symbol.docname, e.symbol.line, - self.display_object_type, sig) - logger.warning(msg, location=signode) + logger.warning( + msg, + e.symbol.docname, + e.symbol.line, + self.display_object_type, + sig, + location=signode, + ) if ast.objectType == 'enumerator': self._add_enumerator_to_parent(ast) @@ -460,7 +465,7 @@ class CPPClassObject(CPPObject): @property def display_object_type(self) -> str: # the distinction between class and struct is only cosmetic - assert self.objtype in ('class', 'struct') + assert self.objtype in {'class', 'struct'} return self.objtype @@ -490,7 +495,7 @@ class CPPNamespaceObject(SphinxDirective): def run(self) -> list[Node]: rootSymbol = self.env.domaindata['cpp']['root_symbol'] - if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + if self.arguments[0].strip() in {'NULL', '0', 'nullptr'}: symbol = rootSymbol stack: list[Symbol] = [] else: @@ -520,7 +525,7 @@ class CPPNamespacePushObject(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + if self.arguments[0].strip() in {'NULL', '0', 'nullptr'}: return [] parser = DefinitionParser(self.arguments[0], location=self.get_location(), @@ -628,7 +633,7 @@ def _render_symbol(self, s: Symbol, maxdepth: int, skipThis: bool, for sChild in s._children: if sChild.declaration is None: continue - if sChild.declaration.objectType in ("templateParam", "functionParam"): + if sChild.declaration.objectType in {"templateParam", "functionParam"}: continue childNodes = self._render_symbol( sChild, maxdepth=maxdepth, skipThis=False, @@ -961,7 +966,7 @@ def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: typ: str, target: str, node: pending_xref, contnode: Element) -> tuple[Element | None, str | None]: # add parens again for those that could be functions - if typ in ('any', 'func'): + if typ in {'any', 'func'}: target += '()' parser = DefinitionParser(target, location=node, config=env.config) try: @@ -969,7 +974,7 @@ def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: except DefinitionError as e: # as arg to stop flake8 from complaining def findWarning(e: Exception) -> tuple[str, Exception]: - if typ != 'any' and typ != 'func': + if typ not in {"any", "func"}: return target, e # hax on top of the paren hax to try to get correct errors parser2 = DefinitionParser(target[:-2], @@ -1081,7 +1086,7 @@ def checkType() -> bool: if (env.config.add_function_parentheses and typ == 'func' and title.endswith('operator()')): addParen += 1 - if (typ in ('any', 'func') and + if (typ in {'any', 'func'} and title.endswith('operator') and displayName.endswith('operator()')): addParen += 1 diff --git a/sphinx/domains/cpp/_ast.py b/sphinx/domains/cpp/_ast.py index e977563442c..e4c92b22a39 100644 --- a/sphinx/domains/cpp/_ast.py +++ b/sphinx/domains/cpp/_ast.py @@ -270,7 +270,7 @@ def describe_signature(self, signode: TextElement, mode: str, assert len(self.names) == 1 assert not self.templates[0] self.names[0].describe_signature(signode, 'param', env, '', symbol) - elif mode in ('markType', 'lastIsName', 'markName'): + elif mode in {'markType', 'lastIsName', 'markName'}: # Each element should be a pending xref targeting the complete # prefix. however, only the identifier part should be a link, such # that template args can be a link as well. @@ -309,7 +309,7 @@ def describe_signature(self, signode: TextElement, mode: str, dest += addnodes.desc_sig_space() first = False txt_nne = str(nne) - if txt_nne != '': + if txt_nne: if nne.templateArgs and iTemplateParams < len(templateParams): templateParamsPrefix += str(templateParams[iTemplateParams]) iTemplateParams += 1 @@ -1541,7 +1541,7 @@ def get_id(self, version: int) -> str: return ids[self.op] def _stringify(self, transform: StringifyTransform) -> str: - if self.op in ('new', 'new[]', 'delete', 'delete[]') or self.op[0] in "abcnox": + if self.op in {'new', 'new[]', 'delete', 'delete[]'} or self.op[0] in "abcnox": return 'operator ' + self.op else: return 'operator' + self.op @@ -1549,7 +1549,7 @@ def _stringify(self, transform: StringifyTransform) -> str: def _describe_identifier(self, signode: TextElement, identnode: TextElement, env: BuildEnvironment, symbol: Symbol) -> None: signode += addnodes.desc_sig_keyword('operator', 'operator') - if self.op in ('new', 'new[]', 'delete', 'delete[]') or self.op[0] in "abcnox": + if self.op in {'new', 'new[]', 'delete', 'delete[]'} or self.op[0] in "abcnox": signode += addnodes.desc_sig_space() identnode += addnodes.desc_sig_operator(self.op, self.op) @@ -2099,7 +2099,7 @@ def _add_anno(signode: TextElement, text: str) -> None: signode += addnodes.desc_sig_space() signode += addnodes.desc_sig_punctuation('=', '=') signode += addnodes.desc_sig_space() - assert self.initializer in ('0', 'delete', 'default') + assert self.initializer in {'0', 'delete', 'default'} if self.initializer == '0': signode += addnodes.desc_sig_literal_number('0', '0') else: @@ -3896,6 +3896,9 @@ def __eq__(self, other: object) -> bool: and self.parameterPack == other.parameterPack ) + def __hash__(self) -> int: + return hash((self.param, self.parameterPack)) + @property def name(self) -> ASTNestedName: id = self.get_identifier() @@ -4260,6 +4263,19 @@ def __eq__(self, other: object) -> bool: and self.enumeratorScopedSymbol == other.enumeratorScopedSymbol ) + def __hash__(self) -> int: + return hash(( + self.objectType, + self.directiveType, + self.visibility, + self.templatePrefix, + self.declaration, + self.trailingRequiresClause, + self.semicolon, + self.symbol, + self.enumeratorScopedSymbol, + )) + def clone(self) -> ASTDeclaration: templatePrefixClone = self.templatePrefix.clone() if self.templatePrefix else None trailingRequiresClasueClone = self.trailingRequiresClause.clone() \ @@ -4374,7 +4390,7 @@ def describe_signature(self, signode: desc_signature, mode: str, elif self.objectType in {'member', 'function'}: pass elif self.objectType == 'class': - assert self.directiveType in ('class', 'struct') + assert self.directiveType in {'class', 'struct'} mainDeclNode += addnodes.desc_sig_keyword(self.directiveType, self.directiveType) mainDeclNode += addnodes.desc_sig_space() elif self.objectType == 'union': @@ -4423,6 +4439,9 @@ def __eq__(self, other: object) -> bool: and self.templatePrefix == other.templatePrefix ) + def __hash__(self) -> int: + return hash((self.nestedName, self.templatePrefix)) + def _stringify(self, transform: StringifyTransform) -> str: res = [] if self.templatePrefix: diff --git a/sphinx/domains/cpp/_parser.py b/sphinx/domains/cpp/_parser.py index d0d70221a45..5eedd078d9b 100644 --- a/sphinx/domains/cpp/_parser.py +++ b/sphinx/domains/cpp/_parser.py @@ -468,7 +468,7 @@ def parser() -> ASTExpression: # | typename-specifier "(" expression-list [opt] ")" # | typename-specifier braced-init-list self.skip_ws() - if self.current_char != '(' and self.current_char != '{': + if self.current_char not in {'(', '{'}: self.fail("Expecting '(' or '{' after type in cast expression.") except DefinitionError as eInner: self.pos = pos @@ -483,7 +483,7 @@ def parser() -> ASTExpression: postFixes: list[ASTPostfixOp] = [] while True: self.skip_ws() - if prefixType in ('expr', 'cast', 'typeid'): + if prefixType in {'expr', 'cast', 'typeid'}: if self.skip_string_and_ws('['): expr = self._parse_expression() self.skip_ws() @@ -967,15 +967,15 @@ def _parse_simple_type_specifiers(self) -> ASTTrailingTypeSpecFundamental: while self.match(_simple_type_specifiers_re): t = self.matched_text names.append(t) - if t in ('auto', 'void', 'bool', + if t in {'auto', 'void', 'bool', 'char', 'wchar_t', 'char8_t', 'char16_t', 'char32_t', 'int', '__int64', '__int128', 'float', 'double', - '__float80', '_Float64x', '__float128', '_Float128'): + '__float80', '_Float64x', '__float128', '_Float128'}: if typ is not None: self.fail(f"Can not have both {t} and {typ}.") typ = t - elif t in ('signed', 'unsigned'): + elif t in {'signed', 'unsigned'}: if signedness is not None: self.fail(f"Can not have both {t} and {signedness}.") signedness = t @@ -987,7 +987,7 @@ def _parse_simple_type_specifiers(self) -> ASTTrailingTypeSpecFundamental: if len(width) != 0 and width[0] != 'long': self.fail(f"Can not have both {t} and {width[0]}.") width.append(t) - elif t in ('_Imaginary', '_Complex'): + elif t in {'_Imaginary', '_Complex'}: if modifier is not None: self.fail(f"Can not have both {t} and {modifier}.") modifier = t @@ -995,9 +995,9 @@ def _parse_simple_type_specifiers(self) -> ASTTrailingTypeSpecFundamental: if len(names) == 0: return None - if typ in ('auto', 'void', 'bool', + if typ in {'auto', 'void', 'bool', 'wchar_t', 'char8_t', 'char16_t', 'char32_t', - '__float80', '_Float64x', '__float128', '_Float128'): + '__float80', '_Float64x', '__float128', '_Float128'}: if modifier is not None: self.fail(f"Can not have both {typ} and {modifier}.") if signedness is not None: @@ -1012,7 +1012,7 @@ def _parse_simple_type_specifiers(self) -> ASTTrailingTypeSpecFundamental: elif typ == 'int': if modifier is not None: self.fail(f"Can not have both {typ} and {modifier}.") - elif typ in ('__int64', '__int128'): + elif typ in {'__int64', '__int128'}: if modifier is not None: self.fail(f"Can not have both {typ} and {modifier}.") if len(width) != 0: @@ -1211,7 +1211,7 @@ def _parse_decl_specs_simple(self, outer: str, typed: bool) -> ASTDeclSpecsSimpl if volatile: continue if not storage: - if outer in ('member', 'function'): + if outer in {'member', 'function'}: if self.skip_word('static'): storage = 'static' continue @@ -1225,11 +1225,11 @@ def _parse_decl_specs_simple(self, outer: str, typed: bool) -> ASTDeclSpecsSimpl if self.skip_word('register'): storage = 'register' continue - if not inline and outer in ('function', 'member'): + if not inline and outer in {'function', 'member'}: inline = self.skip_word('inline') if inline: continue - if not constexpr and outer in ('member', 'function'): + if not constexpr and outer in {'member', 'function'}: constexpr = self.skip_word("constexpr") if constexpr: continue @@ -1281,7 +1281,7 @@ def _parse_decl_specs_simple(self, outer: str, typed: bool) -> ASTDeclSpecsSimpl def _parse_decl_specs(self, outer: str, typed: bool = True) -> ASTDeclSpecs: if outer: - if outer not in ('type', 'member', 'function', 'templateParam'): + if outer not in {'type', 'member', 'function', 'templateParam'}: raise Exception('Internal error, unknown outer "%s".' % outer) """ storage-class-specifier function-specifier "constexpr" @@ -1364,7 +1364,7 @@ def _parse_declarator(self, named: bool | str, paramMode: str, typed: bool = True, ) -> ASTDeclarator: # 'typed' here means 'parse return type stuff' - if paramMode not in ('type', 'function', 'operatorCast', 'new'): + if paramMode not in {'type', 'function', 'operatorCast', 'new'}: raise Exception( "Internal error, unknown paramMode '%s'." % paramMode) prevErrors = [] @@ -1532,12 +1532,12 @@ def _parse_type(self, named: bool | str, outer: str | None = None) -> ASTType: outer == operatorCast: annoying case, we should not take the params """ if outer: # always named - if outer not in ('type', 'member', 'function', - 'operatorCast', 'templateParam'): + if outer not in {'type', 'member', 'function', + 'operatorCast', 'templateParam'}: raise Exception('Internal error, unknown outer "%s".' % outer) if outer != 'operatorCast': assert named - if outer in ('type', 'function'): + if outer in {'type', 'function'}: # We allow type objects to just be a name. # Some functions don't have normal return types: constructors, # destructors, cast operators @@ -1616,7 +1616,7 @@ def _parse_type_with_init( self, named: bool | str, outer: str) -> ASTTypeWithInit | ASTTemplateParamConstrainedTypeWithInit: if outer: - assert outer in ('type', 'member', 'function', 'templateParam') + assert outer in {'type', 'member', 'function', 'templateParam'} type = self._parse_type(outer=outer, named=named) if outer != 'templateParam': init = self._parse_initializer(outer=outer) @@ -1632,7 +1632,7 @@ def _parse_type_with_init( # we parsed an expression, so we must have a , or a >, # otherwise the expression didn't get everything self.skip_ws() - if self.current_char != ',' and self.current_char != '>': + if self.current_char not in {',', '>'}: # pretend it didn't happen self.pos = pos init = None @@ -1993,12 +1993,12 @@ def _check_template_consistency(self, nestedName: ASTNestedName, return templatePrefix def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclaration: - if objectType not in ('class', 'union', 'function', 'member', 'type', - 'concept', 'enum', 'enumerator'): + if objectType not in {'class', 'union', 'function', 'member', 'type', + 'concept', 'enum', 'enumerator'}: raise Exception('Internal error, unknown objectType "%s".' % objectType) - if directiveType not in ('class', 'struct', 'union', 'function', 'member', 'var', + if directiveType not in {'class', 'struct', 'union', 'function', 'member', 'var', 'type', 'concept', - 'enum', 'enum-struct', 'enum-class', 'enumerator'): + 'enum', 'enum-struct', 'enum-class', 'enumerator'}: raise Exception('Internal error, unknown directiveType "%s".' % directiveType) visibility = None templatePrefix = None @@ -2009,7 +2009,7 @@ def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclarati if self.match(_visibility_re): visibility = self.matched_text - if objectType in ('type', 'concept', 'member', 'function', 'class', 'union'): + if objectType in {'type', 'concept', 'member', 'function', 'class', 'union'}: templatePrefix = self._parse_template_declaration_prefix(objectType) if objectType == 'type': diff --git a/sphinx/domains/cpp/_symbol.py b/sphinx/domains/cpp/_symbol.py index ef5a405881a..1cb716955cb 100644 --- a/sphinx/domains/cpp/_symbol.py +++ b/sphinx/domains/cpp/_symbol.py @@ -792,14 +792,19 @@ def unconditionalAdd(self: Symbol, otherChild: Symbol) -> None: name = str(ourChild.declaration) msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" "Declaration is '.. cpp:%s:: %s'.") - msg = msg % (ourChild.docname, ourChild.line, - ourChild.declaration.directiveType, name) - logger.warning(msg, location=(otherChild.docname, otherChild.line)) + logger.warning( + msg, + ourChild.docname, + ourChild.line, + ourChild.declaration.directiveType, + name, + location=(otherChild.docname, otherChild.line), + ) else: if (otherChild.declaration.objectType == ourChild.declaration.objectType and otherChild.declaration.objectType in - ('templateParam', 'functionParam') and + {'templateParam', 'functionParam'} and ourChild.parent.declaration == otherChild.parent.declaration): # `ourChild` was just created during merging by the call # to `_fill_empty` on the parent and can be ignored. diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index c5c0cdc3375..ec81375a6da 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -324,7 +324,7 @@ def run(self) -> list[Node]: domain = self.env.domains.javascript_domain node_id = make_id(self.env, self.state.document, 'module', mod_name) - domain.note_module(mod_name, node_id) + domain.note_module(modname=mod_name, node_id=node_id) # Make a duplicate entry in 'objects' to facilitate searching for # the module in JavaScriptDomain.find_obj() domain.note_object(mod_name, 'module', node_id, diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index 9e6db18ddd9..19e050739db 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -96,7 +96,7 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, ) -> Element | None: - assert typ in ('eq', 'numref') + assert typ in {'eq', 'numref'} result = self.equations.get(target) if result: docname, number = result diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py index d84a87f97f3..eaa3b158d38 100644 --- a/sphinx/domains/python/__init__.py +++ b/sphinx/domains/python/__init__.py @@ -471,11 +471,13 @@ def run(self) -> list[Node]: self.set_source_info(target) self.state.document.note_explicit_target(target) - domain.note_module(modname, - node_id, - self.options.get('synopsis', ''), - self.options.get('platform', ''), - 'deprecated' in self.options) + domain.note_module( + name=modname, + node_id=node_id, + synopsis=self.options.get('synopsis', ''), + platform=self.options.get('platform', ''), + deprecated='deprecated' in self.options, + ) domain.note_object(modname, 'module', node_id, location=target) # the platform and synopsis aren't printed; in fact, they are only @@ -556,26 +558,32 @@ class PythonModuleIndex(Index): name = 'modindex' localname = _('Python Module Index') shortname = _('modules') + domain: PythonDomain def generate(self, docnames: Iterable[str] | None = None, ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + doc_names = frozenset(docnames) if docnames is not None else None + content: dict[str, list[IndexEntry]] = {} # list of prefixes to ignore - ignores: list[str] = self.domain.env.config['modindex_common_prefix'] - ignores = sorted(ignores, key=len, reverse=True) + ignores: list[str] = sorted( + self.domain.env.config['modindex_common_prefix'], key=len, reverse=True + ) + # list of all modules, sorted by module name - modules = sorted(self.domain.data['modules'].items(), - key=lambda x: x[0].lower()) + modules = sorted(self.domain.modules.items(), key=lambda t: t[0].lower()) + # sort out collapsible modules prev_modname = '' - num_toplevels = 0 - for modname, (docname, node_id, synopsis, platforms, deprecated) in modules: - if docnames and docname not in docnames: + + num_top_levels = 0 + for modname, module in modules: + if doc_names and module.docname not in doc_names: continue for ignore in ignores: if modname.startswith(ignore): - modname = modname[len(ignore):] + modname = modname.removeprefix(ignore) stripped = ignore break else: @@ -587,32 +595,55 @@ def generate(self, docnames: Iterable[str] | None = None, entries = content.setdefault(modname[0].lower(), []) - package = modname.split('.')[0] + package = modname.split('.', maxsplit=1)[0] if package != modname: # it's a submodule if prev_modname == package: # first submodule - make parent a group head if entries: last = entries[-1] - entries[-1] = IndexEntry(last[0], 1, last[2], last[3], - last[4], last[5], last[6]) + entries[-1] = IndexEntry( + name=last.name, + subtype=1, + docname=last.docname, + anchor=last.anchor, + extra=last.extra, + qualifier=last.qualifier, + descr=last.descr, + ) elif not prev_modname.startswith(package): # submodule without parent in list, add dummy entry - entries.append(IndexEntry(stripped + package, 1, '', '', '', '', '')) + dummy_entry = IndexEntry( + name=stripped + package, + subtype=1, + docname='', + anchor='', + extra='', + qualifier='', + descr='', + ) + entries.append(dummy_entry) subtype = 2 else: - num_toplevels += 1 + num_top_levels += 1 subtype = 0 - qualifier = _('Deprecated') if deprecated else '' - entries.append(IndexEntry(stripped + modname, subtype, docname, - node_id, platforms, qualifier, synopsis)) + entry = IndexEntry( + name=stripped + modname, + subtype=subtype, + docname=module.docname, + anchor=module.node_id, + extra=module.platform, + qualifier=_('Deprecated') if module.deprecated else '', + descr=module.synopsis, + ) + entries.append(entry) prev_modname = modname # apply heuristics when to collapse modindex at page load: # only collapse if number of toplevel modules is larger than # number of submodules - collapse = len(modules) - num_toplevels < num_toplevels + collapse = len(modules) - num_top_levels < num_top_levels # sort by first letter sorted_content = sorted(content.items()) @@ -704,22 +735,32 @@ def note_object(self, name: str, objtype: str, node_id: str, def modules(self) -> dict[str, ModuleEntry]: return self.data.setdefault('modules', {}) # modname -> ModuleEntry - def note_module(self, name: str, node_id: str, synopsis: str, - platform: str, deprecated: bool) -> None: + def note_module( + self, name: str, node_id: str, synopsis: str, platform: str, deprecated: bool + ) -> None: """Note a python module for cross reference. .. versionadded:: 2.1 """ - self.modules[name] = ModuleEntry(self.env.docname, node_id, - synopsis, platform, deprecated) + self.modules[name] = ModuleEntry( + docname=self.env.docname, + node_id=node_id, + synopsis=synopsis, + platform=platform, + deprecated=deprecated, + ) def clear_doc(self, docname: str) -> None: - for fullname, obj in list(self.objects.items()): - if obj.docname == docname: - del self.objects[fullname] - for modname, mod in list(self.modules.items()): - if mod.docname == docname: - del self.modules[modname] + to_remove = [ + fullname for fullname, obj in self.objects.items() if obj.docname == docname + ] + for fullname in to_remove: + del self.objects[fullname] + to_remove = [ + modname for modname, mod in self.modules.items() if mod.docname == docname + ] + for fullname in to_remove: + del self.modules[fullname] def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX check duplicates? @@ -850,9 +891,10 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui continue if obj[2] == 'module': - results.append(('py:mod', - self._make_module_refnode(builder, fromdocname, - name, contnode))) + results.append(( + 'py:mod', + self._make_module_refnode(builder, fromdocname, name, contnode) + )) else: # determine the content of the reference by conditions content = find_pending_xref_condition(node, 'resolved') @@ -870,16 +912,18 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, contnode: Node) -> Element: # get additional info for modules - module = self.modules[name] - title = name + module: ModuleEntry = self.modules[name] + title_parts = [name] if module.synopsis: - title += ': ' + module.synopsis + title_parts.append(f': {module.synopsis}') if module.deprecated: - title += _(' (deprecated)') + title_parts.append(_(' (deprecated)')) if module.platform: - title += ' (' + module.platform + ')' - return make_refnode(builder, fromdocname, module.docname, module.node_id, - contnode, title) + title_parts.append(f' ({module.platform})') + title = ''.join(title_parts) + return make_refnode( + builder, fromdocname, module.docname, module.node_id, contnode, title + ) def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: for modname, mod in self.modules.items(): @@ -913,9 +957,9 @@ def istyping(s: str) -> bool: if node.get('refdomain') != 'py': return None - elif node.get('reftype') in ('class', 'obj') and node.get('reftarget') == 'None': + elif node.get('reftype') in {'class', 'obj'} and node.get('reftarget') == 'None': return contnode - elif node.get('reftype') in ('class', 'obj', 'exc'): + elif node.get('reftype') in {'class', 'obj', 'exc'}: reftarget = node.get('reftarget') if inspect.isclass(getattr(builtins, reftarget, None)): # built-in class diff --git a/sphinx/domains/python/_annotations.py b/sphinx/domains/python/_annotations.py index 35525f6b1b3..0c211ade672 100644 --- a/sphinx/domains/python/_annotations.py +++ b/sphinx/domains/python/_annotations.py @@ -140,7 +140,7 @@ def unparse(node: ast.AST) -> list[Node]: result.append(addnodes.desc_sig_punctuation('', ']')) # Wrap the Text nodes inside brackets by literal node if the subscript is a Literal - if result[0] in ('Literal', 'typing.Literal'): + if result[0] in {'Literal', 'typing.Literal'}: for i, subnode in enumerate(result[1:], start=1): if isinstance(subnode, nodes.Text): result[i] = nodes.literal('', '', subnode) @@ -436,9 +436,9 @@ def _parse_arglist( if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) - if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, + if param.kind == param.KEYWORD_ONLY and last_kind in {param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY, - None): + None}: # PEP-3102: Separator for Keyword Only Parameter: * params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '*')) diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py index 1953f7c3a04..470a1c05428 100644 --- a/sphinx/domains/std/__init__.py +++ b/sphinx/domains/std/__init__.py @@ -749,22 +749,25 @@ def anonlabels(self) -> dict[str, tuple[str, str]]: return self.data.setdefault('anonlabels', {}) # labelname -> docname, labelid def clear_doc(self, docname: str) -> None: - key: Any = None - for key, (fn, _l) in list(self.progoptions.items()): - if fn == docname: - del self.progoptions[key] - for key, (fn, _l) in list(self.objects.items()): - if fn == docname: - del self.objects[key] - for key, (fn, _l) in list(self._terms.items()): - if fn == docname: - del self._terms[key] - for key, (fn, _l, _l) in list(self.labels.items()): - if fn == docname: - del self.labels[key] - for key, (fn, _l) in list(self.anonlabels.items()): - if fn == docname: - del self.anonlabels[key] + to_remove1 = [key for key, (fn, _l) in self.progoptions.items() if fn == docname] + for key1 in to_remove1: + del self.progoptions[key1] + + to_remove2 = [key for key, (fn, _l) in self.objects.items() if fn == docname] + for key2 in to_remove2: + del self.objects[key2] + + to_remove3 = [key for key, (fn, _l) in self._terms.items() if fn == docname] + for key3 in to_remove3: + del self._terms[key3] + + to_remove3 = [key for key, (fn, _l, _l) in self.labels.items() if fn == docname] + for key3 in to_remove3: + del self.labels[key3] + + to_remove3 = [key for key, (fn, _l) in self.anonlabels.items() if fn == docname] + for key3 in to_remove3: + del self.anonlabels[key3] def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX duplicates? diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 871810bfeff..34ef4cc8066 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -54,10 +54,6 @@ 'image_loading': 'link', 'embed_stylesheet': False, 'cloak_email_addresses': True, - 'cve_base_url': 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-', - 'cve_references': None, - 'cwe_base_url': 'https://cwe.mitre.org/data/definitions/', - 'cwe_references': None, 'pep_base_url': 'https://peps.python.org/', 'pep_references': None, 'rfc_base_url': 'https://datatracker.ietf.org/doc/html/', @@ -467,7 +463,7 @@ def find_files(self, config: Config, builder: Builder) -> None: for docname in self.found_docs: domain = docname_to_domain(docname, self.config.gettext_compact) if domain in mo_paths: - self.dependencies[docname].add(mo_paths[domain]) + self.dependencies[docname].add(str(mo_paths[domain])) except OSError as exc: raise DocumentError( __('Failed to scan documents in %s: %r') % (self.srcdir, exc) diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index 42ae170b233..6e4b6ee51af 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -231,8 +231,7 @@ def _key_func_1(entry: tuple[str, _IndexEntry]) -> tuple[tuple[int, str], str]: # using the specified category key to sort key = category_key lc_key = unicodedata.normalize('NFD', key.lower()) - if lc_key.startswith('\N{RIGHT-TO-LEFT MARK}'): - lc_key = lc_key[1:] + lc_key = lc_key.removeprefix('\N{RIGHT-TO-LEFT MARK}') if not lc_key[0:1].isalpha() and not lc_key.startswith('_'): # put symbols at the front of the index (0) @@ -248,8 +247,7 @@ def _key_func_1(entry: tuple[str, _IndexEntry]) -> tuple[tuple[int, str], str]: def _key_func_2(entry: tuple[str, _IndexEntryTargets]) -> str: """Sort the sub-index entries""" key = unicodedata.normalize('NFD', entry[0].lower()) - if key.startswith('\N{RIGHT-TO-LEFT MARK}'): - key = key[1:] + key = key.removeprefix('\N{RIGHT-TO-LEFT MARK}') if key[0:1].isalpha() or key.startswith('_'): key = chr(127) + key return key @@ -263,8 +261,7 @@ def _group_by_func(entry: tuple[str, _IndexEntry]) -> str: return category_key # now calculate the key - if key.startswith('\N{RIGHT-TO-LEFT MARK}'): - key = key[1:] + key = key.removeprefix('\N{RIGHT-TO-LEFT MARK}') letter = unicodedata.normalize('NFD', key[0])[0].upper() if letter.isalpha() or letter == '_': return letter diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index f2c1ed93782..c5d2b0b248b 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -10,6 +10,7 @@ import functools import operator import re +import sys from inspect import Parameter, Signature from typing import TYPE_CHECKING, Any, NewType, TypeVar @@ -106,7 +107,7 @@ def __contains__(self, item: Any) -> bool: def members_option(arg: Any) -> object | list[str]: """Used to convert the :members: option to auto directives.""" - if arg in (None, True): + if arg in {None, True}: return ALL elif arg is False: return None @@ -116,14 +117,14 @@ def members_option(arg: Any) -> object | list[str]: def exclude_members_option(arg: Any) -> object | set[str]: """Used to convert the :exclude-members: option.""" - if arg in (None, True): + if arg in {None, True}: return EMPTY return {x.strip() for x in arg.split(',') if x.strip()} def inherited_members_option(arg: Any) -> set[str]: """Used to convert the :inherited-members: option to auto directives.""" - if arg in (None, True): + if arg in {None, True}: return {'object'} elif arg: return {x.strip() for x in arg.split(',')} @@ -133,9 +134,9 @@ def inherited_members_option(arg: Any) -> set[str]: def member_order_option(arg: Any) -> str | None: """Used to convert the :member-order: option to auto directives.""" - if arg in (None, True): + if arg in {None, True}: return None - elif arg in ('alphabetical', 'bysource', 'groupwise'): + elif arg in {'alphabetical', 'bysource', 'groupwise'}: return arg else: raise ValueError(__('invalid value for member-order option: %s') % arg) @@ -143,7 +144,7 @@ def member_order_option(arg: Any) -> str | None: def class_doc_from_option(arg: Any) -> str | None: """Used to convert the :class-doc-from: option to autoclass directives.""" - if arg in ('both', 'class', 'init'): + if arg in {'both', 'class', 'init'}: return arg else: raise ValueError(__('invalid value for class-doc-from option: %s') % arg) @@ -153,7 +154,7 @@ def class_doc_from_option(arg: Any) -> str | None: def annotation_option(arg: Any) -> Any: - if arg in (None, True): + if arg in {None, True}: # suppress showing the representation of the object return SUPPRESS else: @@ -177,8 +178,9 @@ def merge_members_option(options: dict) -> None: members = options.setdefault('members', []) for key in ('private-members', 'special-members'): - if key in options and options[key] not in (ALL, None): - for member in options[key]: + other_members = options.get(key) + if other_members is not None and other_members is not ALL: + for member in other_members: if member not in members: members.append(member) @@ -186,7 +188,7 @@ def merge_members_option(options: dict) -> None: # Some useful event listener factories for autodoc-process-docstring. def cut_lines( - pre: int, post: int = 0, what: str | list[str] | None = None + pre: int, post: int = 0, what: Sequence[str] | None = None ) -> _AutodocProcessDocstringListener: """Return a listener that removes the first *pre* and last *post* lines of every docstring. If *what* is a sequence of strings, @@ -199,7 +201,12 @@ def cut_lines( This can (and should) be used in place of :confval:`automodule_skip_lines`. """ - what_unique = frozenset(what or ()) + if not what: + what_unique: frozenset[str] = frozenset() + elif isinstance(what, str): # strongly discouraged + what_unique = frozenset({what}) + else: + what_unique = frozenset(what) def process( app: Sphinx, @@ -209,7 +216,7 @@ def process( options: dict[str, bool], lines: list[str], ) -> None: - if what_ not in what_unique: + if what_unique and what_ not in what_unique: return del lines[:pre] if post: @@ -581,7 +588,7 @@ def process_doc(self, docstrings: list[list[str]]) -> Iterator[str]: self.objtype, self.fullname, self.object, self.options, docstringlines) - if docstringlines and docstringlines[-1] != '': + if docstringlines and docstringlines[-1]: # append a blank line to the end of the docstring docstringlines.append('') @@ -1312,7 +1319,7 @@ def can_document_member( (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) def format_args(self, **kwargs: Any) -> str: - if self.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in {'none', 'description'}: kwargs.setdefault('show_annotation', False) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) @@ -1446,15 +1453,15 @@ def format_args(self, **kwargs: Any) -> str: # Types which have confusing metaclass signatures it would be best not to show. # These are listed by name, rather than storing the objects themselves, to avoid # needing to import the modules. -_METACLASS_CALL_BLACKLIST = [ - 'enum.EnumMeta.__call__', -] +_METACLASS_CALL_BLACKLIST = frozenset({ + 'enum.EnumType.__call__', +}) # Types whose __new__ signature is a pass-through. -_CLASS_NEW_BLACKLIST = [ +_CLASS_NEW_BLACKLIST = frozenset({ 'typing.Generic.__new__', -] +}) class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore[misc] @@ -1538,10 +1545,15 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: # This sequence is copied from inspect._signature_from_callable. # ValueError means that no signature could be found, so we keep going. - # First, we check the obj has a __signature__ attribute - if (hasattr(self.object, '__signature__') and - isinstance(self.object.__signature__, Signature)): - return None, None, self.object.__signature__ + # First, we check if obj has a __signature__ attribute + if hasattr(self.object, '__signature__'): + object_sig = self.object.__signature__ + if isinstance(object_sig, Signature): + return None, None, object_sig + if sys.version_info[:2] in {(3, 12), (3, 13)} and callable(object_sig): + # Support for enum.Enum.__signature__ in Python 3.12 + if isinstance(object_sig_str := object_sig(), str): + return None, None, inspect.signature_from_str(object_sig_str) # Next, let's see if it has an overloaded __call__ defined # in its metaclass @@ -1604,7 +1616,7 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: return None, None, None def format_args(self, **kwargs: Any) -> str: - if self.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in {'none', 'description'}: kwargs.setdefault('show_annotation', False) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) @@ -1786,7 +1798,7 @@ def get_doc(self) -> list[list[str]] | None: # for classes, what the "docstring" is can be controlled via a # config value; the default is only the class docstring - if classdoc_from in ('both', 'init'): + if classdoc_from in {'both', 'init'}: __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, self.config.autodoc_inherit_docstrings, @@ -2042,7 +2054,7 @@ def update_annotations(self, parent: Any) -> None: analyzer = ModuleAnalyzer.for_module(self.modname) analyzer.analyze() for (classname, attrname), annotation in analyzer.annotations.items(): - if classname == '' and attrname not in annotations: + if not classname and attrname not in annotations: annotations[attrname] = annotation except PycodeError: pass @@ -2172,7 +2184,7 @@ def import_object(self, raiseerror: bool = False) -> bool: return ret def format_args(self, **kwargs: Any) -> str: - if self.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in {'none', 'description'}: kwargs.setdefault('show_annotation', False) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index ebdaa984888..4311d428870 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -57,13 +57,9 @@ def _filter_enum_dict( # names that are ignored by default ignore_names = Enum.__dict__.keys() - public_names - def is_native_api(obj: object, name: str) -> bool: - """Check whether *obj* is the same as ``Enum.__dict__[name]``.""" - return unwrap_all(obj) is unwrap_all(Enum.__dict__[name]) - def should_ignore(name: str, value: Any) -> bool: if name in sunder_names: - return is_native_api(value, name) + return _is_native_enum_api(value, name) return name in ignore_names sentinel = object() @@ -101,12 +97,17 @@ def query(name: str, defining_class: type) -> tuple[str, type, Any] | None: special_names &= Enum.__dict__.keys() for name in special_names: if ( - not is_native_api(enum_class_dict[name], name) + not _is_native_enum_api(enum_class_dict[name], name) and (item := query(name, enum_class)) is not None ): yield item +def _is_native_enum_api(obj: object, name: str) -> bool: + """Check whether *obj* is the same as ``Enum.__dict__[name]``.""" + return unwrap_all(obj) is unwrap_all(Enum.__dict__[name]) + + def mangle(subject: Any, name: str) -> str: """Mangle the given name.""" try: diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py index 8242934243e..5c8fc078027 100644 --- a/sphinx/ext/autodoc/preserve_defaults.py +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -161,7 +161,7 @@ def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None: # Consume kw_defaults for kwonly args kw_defaults.pop(0) else: - if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): + if param.kind in {param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD}: default = defaults.pop(0) value = get_default_value(lines, default) if value is None: diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index ed8860cb71e..a2b05aac705 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -45,7 +45,7 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element) -> None: if domain != 'py': return - if app.config.autodoc_typehints not in ('both', 'description'): + if app.config.autodoc_typehints not in {'both', 'description'}: return try: @@ -177,14 +177,14 @@ def augment_descriptions_with_types( elif parts[0] == 'type': name = ' '.join(parts[1:]) has_type.add(name) - elif parts[0] in ('return', 'returns'): + elif parts[0] in {'return', 'returns'}: has_description.add('return') elif parts[0] == 'rtype': has_type.add('return') # Add 'type' for parameters with a description but no declared type. for name, annotation in annotations.items(): - if name in ('return', 'returns'): + if name in {'return', 'returns'}: continue if '*' + name in has_description: diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 19a08920b8d..5fbc51118bf 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -358,7 +358,7 @@ def generate_autosummary_content( if modname is None or qualname is None: modname, qualname = _split_full_qualified_name(name) - if doc.objtype in ('method', 'attribute', 'property'): + if doc.objtype in {'method', 'attribute', 'property'}: ns['class'] = qualname.rsplit('.', 1)[0] if doc.objtype == 'class': @@ -461,7 +461,7 @@ def _get_module_attrs(name: str, members: Any) -> tuple[list[str], list[str]]: analyzer = ModuleAnalyzer.for_module(name) attr_docs = analyzer.find_attr_docs() for namespace, attr_name in attr_docs: - if namespace == '' and attr_name in members: + if not namespace and attr_name in members: attrs.append(attr_name) if not attr_name.startswith('_'): public.append(attr_name) diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 633d438979c..0aead2db480 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -86,7 +86,7 @@ def run(self) -> list[Node]: test = code code = doctestopt_re.sub('', code) nodetype: type[TextElement] = nodes.literal_block - if self.name in ('testsetup', 'testcleanup') or 'hide' in self.options: + if self.name in {'testsetup', 'testcleanup'} or 'hide' in self.options: nodetype = nodes.comment if self.arguments: groups = [x.strip() for x in self.arguments[0].split(',')] @@ -105,7 +105,7 @@ def run(self) -> list[Node]: # don't try to highlight output node['language'] = 'none' node['options'] = {} - if self.name in ('doctest', 'testoutput') and 'options' in self.options: + if self.name in {'doctest', 'testoutput'} and 'options' in self.options: # parse doctest-like output comparison flags option_strings = self.options['options'].replace(',', ' ').split() for option in option_strings: diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index af636c7473d..13bb5ea4111 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -325,7 +325,7 @@ def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: d ) -> tuple[str, str]: format = self.builder.config.graphviz_output_format try: - if format not in ('png', 'svg'): + if format not in {'png', 'svg'}: raise GraphvizError(__("graphviz_output_format must be one of 'png', " "'svg', but is %r") % format) fname, outfn = render_dot(self, code, options, format, prefix, filename) diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index 58da35bfc89..aed8252a5cf 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -96,7 +96,7 @@ def generate_latex_macro(image_format: str, 'fontsize': config.imgmath_font_size, 'baselineskip': int(round(config.imgmath_font_size * 1.2)), 'preamble': config.imgmath_latex_preamble, - # the dvips option is important when imgmath_latex in ["xelatex", "tectonic"], + # the dvips option is important when imgmath_latex in {"xelatex", "tectonic"}, # it has no impact when imgmath_latex="latex" 'tightpage': '' if image_format == 'png' else ',dvips,tightpage', 'math': math, diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 21dda6fe65b..ed2a8c0936b 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -166,7 +166,7 @@ def _import_classes(self, class_names: list[str], currmodule: str) -> list[Any]: def _class_info(self, classes: list[Any], show_builtins: bool, private_bases: bool, parts: int, aliases: dict[str, str] | None, top_classes: Sequence[Any], - ) -> list[tuple[str, str, list[str], str]]: + ) -> list[tuple[str, str, Sequence[str], str | None]]: """Return name and bases for all classes that are ancestors of *classes*. @@ -221,7 +221,11 @@ def recurse(cls: Any) -> None: for cls in classes: recurse(cls) - return list(all_classes.values()) # type: ignore[arg-type] + return [ + (cls_name, fullname, tuple(bases), tooltip) + for (cls_name, fullname, bases, tooltip) + in all_classes.values() + ] def class_name( self, cls: Any, parts: int = 0, aliases: dict[str, str] | None = None, @@ -232,7 +236,7 @@ def class_name( completely general. """ module = cls.__module__ - if module in ('__builtin__', 'builtins'): + if module in {'__builtin__', 'builtins'}: fullname = cls.__name__ else: fullname = f'{module}.{cls.__qualname__}' @@ -312,19 +316,19 @@ def generate_dot(self, name: str, urls: dict[str, str] | None = None, self._format_graph_attrs(g_attrs), ] - for name, fullname, bases, tooltip in sorted(self.class_info): + for cls_name, fullname, bases, tooltip in sorted(self.class_info): # Write the node this_node_attrs = n_attrs.copy() if fullname in urls: - this_node_attrs["URL"] = '"%s"' % urls[fullname] + this_node_attrs["URL"] = f'"{urls[fullname]}"' this_node_attrs["target"] = '"_top"' if tooltip: this_node_attrs["tooltip"] = tooltip - res.append(' "%s" [%s];\n' % (name, self._format_node_attrs(this_node_attrs))) + res.append(f' "{cls_name}" [{self._format_node_attrs(this_node_attrs)}];\n') # Write the edges res.extend( - ' "%s" -> "%s" [%s];\n' % (base_name, name, self._format_node_attrs(e_attrs)) + f' "{base_name}" -> "{cls_name}" [{self._format_node_attrs(e_attrs)}];\n' for base_name in bases ) res.append("}\n") diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 085ce23fb98..b2a85815d6f 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -448,10 +448,10 @@ def _skip_member( """ has_doc = getattr(obj, '__doc__', False) - is_member = what in ('class', 'exception', 'module') + is_member = what in {'class', 'exception', 'module'} if name != '__weakref__' and has_doc and is_member: cls_is_owner = False - if what in ('class', 'exception'): + if what in {'class', 'exception'}: qualname = getattr(obj, '__qualname__', '') cls_path, _, _ = qualname.rpartition('.') if cls_path: diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 3a7f821b2b2..d9f1eb357dd 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -635,7 +635,7 @@ def _load_custom_sections(self) -> None: def _parse(self) -> None: self._parsed_lines = self._consume_empty() - if self._name and self._what in ('attribute', 'data', 'property'): + if self._name and self._what in {'attribute', 'data', 'property'}: res: list[str] = [] with contextlib.suppress(StopIteration): res = self._parse_attribute_docstring() @@ -893,7 +893,7 @@ def _strip_empty(self, lines: list[str]) -> list[str]: def _lookup_annotation(self, _name: str) -> str: if self._config.napoleon_attr_annotations: - if self._what in ('module', 'class', 'exception') and self._obj: + if self._what in {'module', 'class', 'exception'} and self._obj: # cache the class annotations if not hasattr(self, '_annotations'): localns = getattr(self._config, 'autodoc_type_aliases', {}) @@ -1038,7 +1038,7 @@ def is_numeric(token: str) -> bool: location=location, ) type_ = 'literal' - elif token in ('optional', 'default'): + elif token in {'optional', 'default'}: # default is not a official keyword (yet) but supported by the # reference implementation (numpydoc) and widely used type_ = 'control' @@ -1270,7 +1270,7 @@ def _is_section_break(self) -> bool: return ( not self._lines or self._is_section_header() - or (line1 == line2 == '') + or (not line1 and not line2) or ( self._is_in_section and line1 diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index ed27a387511..3621d417b94 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -13,6 +13,7 @@ from sphinx.application import TemplateBridge from sphinx.util import logging +from sphinx.util._pathlib import _StrPath from sphinx.util.osutil import _last_modified_time if TYPE_CHECKING: @@ -26,7 +27,7 @@ def _tobool(val: str) -> bool: if isinstance(val, str): - return val.lower() in ('true', '1', 'yes', 'on') + return val.lower() in {'true', '1', 'yes', 'on'} return bool(val) @@ -170,10 +171,10 @@ def init( # the theme's own dir and its bases' dirs pathchain = theme.get_theme_dirs() # the loader dirs: pathchain + the parent directories for all themes - loaderchain = pathchain + [path.join(p, '..') for p in pathchain] + loaderchain = pathchain + [p.parent for p in pathchain] elif dirs: - pathchain = list(dirs) - loaderchain = list(dirs) + pathchain = list(map(_StrPath, dirs)) + loaderchain = list(map(_StrPath, dirs)) else: pathchain = [] loaderchain = [] @@ -182,7 +183,7 @@ def init( self.templatepathlen = len(builder.config.templates_path) if builder.config.templates_path: cfg_templates_path = [ - path.join(builder.confdir, tp) for tp in builder.config.templates_path + builder.confdir / tp for tp in builder.config.templates_path ] pathchain[0:0] = cfg_templates_path loaderchain[0:0] = cfg_templates_path diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 1d66c8e1e45..d1a70d05b36 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -5,7 +5,7 @@ import locale import sys from gettext import NullTranslations, translation -from os import path +from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -141,11 +141,11 @@ def init( return translator, has_translation -_LOCALE_DIR = path.abspath(path.dirname(__file__)) +_LOCALE_DIR = Path(__file__).parent.resolve() def init_console( - locale_dir: str | None = None, + locale_dir: str | os.PathLike[str] | None = None, catalog: str = 'sphinx', ) -> tuple[NullTranslations, bool]: """Initialize locale for console. diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index da078b2f0f0..4094d7a9b8b 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -2,13 +2,15 @@ from __future__ import annotations +import os import tokenize from importlib import import_module from os import path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from sphinx.errors import PycodeError from sphinx.pycode.parser import Parser +from sphinx.util._pathlib import _StrPath if TYPE_CHECKING: from inspect import Signature @@ -23,7 +25,7 @@ class ModuleAnalyzer: tags: dict[str, tuple[str, int, int]] # cache for analyzer objects -- caches both by module and file name - cache: dict[tuple[str, str], Any] = {} + cache: dict[tuple[Literal['file', 'module'], str | _StrPath], Any] = {} @staticmethod def get_module_source(modname: str) -> tuple[str | None, str | None]: @@ -81,8 +83,9 @@ def for_string( @classmethod def for_file( - cls: type[ModuleAnalyzer], filename: str, modname: str + cls: type[ModuleAnalyzer], filename: str | os.PathLike[str], modname: str ) -> ModuleAnalyzer: + filename = _StrPath(filename) if ('file', filename) in cls.cache: return cls.cache['file', filename] try: @@ -114,9 +117,11 @@ def for_module(cls: type[ModuleAnalyzer], modname: str) -> ModuleAnalyzer: cls.cache['module', modname] = obj return obj - def __init__(self, source: str, modname: str, srcname: str) -> None: + def __init__( + self, source: str, modname: str, srcname: str | os.PathLike[str] + ) -> None: self.modname = modname # name of the module - self.srcname = srcname # name of the source file + self.srcname = str(srcname) # name of the source file # cache the source code as well self.code = source diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 47ce2da1a57..6b04c6f0e21 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -47,14 +47,14 @@ def get_lvar_names(node: ast.AST, self: ast.arg | None = None) -> list[str]: self_id = self.arg node_name = node.__class__.__name__ - if node_name in ('Constant', 'Index', 'Slice', 'Subscript'): + if node_name in {'Constant', 'Index', 'Slice', 'Subscript'}: raise TypeError('%r does not create new variable' % node) if node_name == 'Name': if self is None or node.id == self_id: # type: ignore[attr-defined] return [node.id] # type: ignore[attr-defined] else: raise TypeError('The assignment %r is not instance variable' % node) - elif node_name in ('Tuple', 'List'): + elif node_name in {'Tuple', 'List'}: members = [] for elt in node.elts: # type: ignore[attr-defined] with contextlib.suppress(TypeError): @@ -123,6 +123,9 @@ def __eq__(self, other: Any) -> bool: else: raise ValueError('Unknown value: %r' % other) + def __hash__(self) -> int: + return hash((self.kind, self.value, self.start, self.end, self.source)) + def match(self, *conditions: Any) -> bool: return any(self == candidate for candidate in conditions) diff --git a/sphinx/registry.py b/sphinx/registry.py index da21aefcee3..b4c2478653b 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -17,9 +17,11 @@ from sphinx.parsers import Parser as SphinxParser from sphinx.roles import XRefRole from sphinx.util import logging +from sphinx.util._pathlib import _StrPath from sphinx.util.logging import prefixed_warnings if TYPE_CHECKING: + import os from collections.abc import Callable, Iterator, Sequence from docutils import nodes @@ -100,7 +102,7 @@ def __init__(self) -> None: self.html_assets_policy: str = 'per_page' #: HTML themes - self.html_themes: dict[str, str] = {} + self.html_themes: dict[str, _StrPath] = {} #: js_files; list of JS paths or URLs self.js_files: list[tuple[str | None, dict[str, Any]]] = [] @@ -433,8 +435,8 @@ def add_html_math_renderer( if block_renderers is not None: self.html_block_math_renderers[name] = block_renderers - def add_html_theme(self, name: str, theme_path: str) -> None: - self.html_themes[name] = theme_path + def add_html_theme(self, name: str, theme_path: str | os.PathLike[str]) -> None: + self.html_themes[name] = _StrPath(theme_path) def load_extension(self, app: Sphinx, extname: str) -> None: """Load a Sphinx extension.""" diff --git a/sphinx/roles.py b/sphinx/roles.py index 27ceed29532..aed317177c5 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing import Final from docutils.nodes import Element, Node, TextElement, system_message @@ -197,6 +198,8 @@ def process_link( class CVE(ReferenceRole): + _BASE_URL: Final = 'https://www.cve.org/CVERecord?id=CVE-' + def run(self) -> tuple[list[Node], list[system_message]]: target_id = f'index-{self.env.new_serialno("index")}' entries = [ @@ -233,14 +236,15 @@ def run(self) -> tuple[list[Node], list[system_message]]: return [index, target, reference], [] def build_uri(self) -> str: - base_url = self.inliner.document.settings.cve_base_url ret = self.target.split('#', 1) if len(ret) == 2: - return f'{base_url}{ret[0]}#{ret[1]}' - return f'{base_url}{ret[0]}' + return f'{CVE._BASE_URL}{ret[0]}#{ret[1]}' + return f'{CVE._BASE_URL}{ret[0]}' class CWE(ReferenceRole): + _BASE_URL: Final = 'https://cwe.mitre.org/data/definitions/' + def run(self) -> tuple[list[Node], list[system_message]]: target_id = f'index-{self.env.new_serialno("index")}' entries = [ @@ -277,11 +281,10 @@ def run(self) -> tuple[list[Node], list[system_message]]: return [index, target, reference], [] def build_uri(self) -> str: - base_url = self.inliner.document.settings.cwe_base_url ret = self.target.split('#', 1) if len(ret) == 2: - return f'{base_url}{int(ret[0])}.html#{ret[1]}' - return f'{base_url}{int(ret[0])}.html' + return f'{CWE._BASE_URL}{int(ret[0])}.html#{ret[1]}' + return f'{CWE._BASE_URL}{int(ret[0])}.html' class PEP(ReferenceRole): diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 7207b6ecd36..82081cbe2e9 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -6,6 +6,7 @@ import functools import html import json +import os import pickle import re from importlib import import_module @@ -163,7 +164,8 @@ class _JavaScriptIndex: SUFFIX = ')' def dumps(self, data: Any) -> str: - return self.PREFIX + json.dumps(data, sort_keys=True) + self.SUFFIX + data_json = json.dumps(data, separators=(',', ':'), sort_keys=True) + return self.PREFIX + data_json + self.SUFFIX def loads(self, s: str) -> Any: data = s[len(self.PREFIX) : -len(self.SUFFIX)] @@ -210,7 +212,7 @@ class WordCollector(nodes.NodeVisitor): def __init__(self, document: nodes.document, lang: SearchLanguage) -> None: super().__init__(document) self.found_words: list[str] = [] - self.found_titles: list[tuple[str, str]] = [] + self.found_titles: list[tuple[str, str | None]] = [] self.found_title_words: list[str] = [] self.lang = lang @@ -241,8 +243,10 @@ def dispatch_visit(self, node: Node) -> None: self.found_words.extend(self.lang.split(node.astext())) elif isinstance(node, nodes.title): title = node.astext() - ids = node.parent['ids'] - self.found_titles.append((title, ids[0] if ids else None)) + if ids := node.parent['ids']: + self.found_titles.append((title, ids[0])) + else: + self.found_titles.append((title, None)) self.found_title_words.extend(self.lang.split(title)) elif isinstance(node, Element) and _is_meta_keywords(node, self.lang.lang): # type: ignore[arg-type] keywords = node['content'] @@ -471,11 +475,15 @@ def prune(self, docnames: Iterable[str]) -> None: wordnames.intersection_update(docnames) def feed( - self, docname: str, filename: str, title: str, doctree: nodes.document + self, + docname: str, + filename: str | os.PathLike[str], + title: str, + doctree: nodes.document, ) -> None: """Feed a doctree to the index.""" self._titles[docname] = title - self._filenames[docname] = filename + self._filenames[docname] = os.fspath(filename) word_store = self._word_collector(doctree) diff --git a/sphinx/search/zh.py b/sphinx/search/zh.py index 4905eb84474..28251e8c1db 100644 --- a/sphinx/search/zh.py +++ b/sphinx/search/zh.py @@ -2,8 +2,8 @@ from __future__ import annotations -import os import re +from pathlib import Path import snowballstemmer @@ -13,8 +13,10 @@ import jieba # type: ignore[import-not-found] JIEBA = True + JIEBA_DEFAULT_DICT = Path(jieba.__file__).parent / jieba.DEFAULT_DICT_NAME except ImportError: JIEBA = False + JIEBA_DEFAULT_DICT = Path() english_stopwords = set( """ @@ -234,8 +236,8 @@ def __init__(self, options: dict[str, str]) -> None: def init(self, options: dict[str, str]) -> None: if JIEBA: - dict_path = options.get('dict') - if dict_path and os.path.isfile(dict_path): + dict_path = options.get('dict', JIEBA_DEFAULT_DICT) + if dict_path and Path(dict_path).is_file(): jieba.load_userdict(dict_path) self.stemmer = snowballstemmer.stemmer('english') diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 0d9a5ddc747..0d52676c4fe 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -9,7 +9,7 @@ % by the Sphinx LaTeX writer. \NeedsTeXFormat{LaTeX2e}[1995/12/01] -\ProvidesPackage{sphinx}[2024/07/28 v8.1.0 Sphinx LaTeX package (sphinx-doc)] +\ProvidesPackage{sphinx}[2024/10/11 v8.1.1 Sphinx LaTeX package (sphinx-doc)] % provides \ltx@ifundefined % (many packages load ltxcmds: graphicx does for pdftex and lualatex but @@ -885,15 +885,12 @@ {\DeclareStringOption[fontawesome]{iconpackage}} {\DeclareStringOption[none]{iconpackage}}% }% -\newcommand\spx@faIcon[3][]{}% -% The hacky definition of \spx@faIcon above is to let it by default swallow -% the icon macro and the \sphinxtitlerowaftericonspacecmd (see -% \sphinxdotitlerowwithicon in sphinxlatexadmonitions.sty) which inserts -% a space between it and title. See how \spx@faIcon is used below. -% -% If user sets a title-icon key to some LaTeX code of their own, of course -% \spx@faIcon is not executed and the inserted space will thus be there, as -% expected. +\newcommand\spx@faIcon[2][]{}% +% The above \spx@faIcon which gobbles one mandatory and one optional +% argument is put into use only if both fontawesome5 and fontawesome +% LaTeX packages are not available, as part of the defaults for the +% div.*_title-icon keys (these keys can be redefined via the sphinxsetup +% interface). % \def\spxstring@fontawesome{fontawesome} \def\spxstring@fontawesomev{fontawesome5} @@ -920,8 +917,8 @@ \let\faicon@pen\faPencil \fi % if neither has been required, \spx@faIcon will simply swallow - % its argument (and follwing space macro) and it is up to user - % to set the keys appropriately. + % its argument and it is up to user + % to set the various div.*_title-icon keys appropriately. \fi\fi % }% {% diff --git a/sphinx/texinputs/sphinxlatexadmonitions.sty b/sphinx/texinputs/sphinxlatexadmonitions.sty index 2d50b2eb313..76fef5a8c4f 100644 --- a/sphinx/texinputs/sphinxlatexadmonitions.sty +++ b/sphinx/texinputs/sphinxlatexadmonitions.sty @@ -1,7 +1,7 @@ %% NOTICES AND ADMONITIONS % % change this info string if making any custom modification -\ProvidesPackage{sphinxlatexadmonitions}[2024/07/28 v8.1.0 admonitions] +\ProvidesPackage{sphinxlatexadmonitions}[2024/10/11 v8.1.1 admonitions] % Provides support for this output mark-up from Sphinx latex writer: % @@ -341,9 +341,11 @@ \ifdim\wd\z@>\z@ \textcolor{sphinx#1TtlFgColor}{% \@nameuse{sphinx#1TtlIcon}% - % This macro is located here and not after the closing brace - % for reasons of fall-back \spx@faIcon definition in sphinx.sty - % in case fontawesome5 package not found. + % The next macro is located here for legacy reasons of earlier + % functioning of \spx@faIcon. When fontawesome{5,}.sty both + % are unavailable, it (formerly) gobbled this next macro. + % We leave it here now although it could be moved to after + % the closing brace. \sphinxtitlerowaftericonspacecmd }% \fi diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 2c774d17aff..aaf078d2b91 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -547,8 +547,9 @@ const Search = { // set score for the word in each file recordFiles.forEach((file) => { - if (!scoreMap.has(file)) scoreMap.set(file, {}); - scoreMap.get(file)[word] = record.score; + if (!scoreMap.has(file)) scoreMap.set(file, new Map()); + const fileScores = scoreMap.get(file); + fileScores.set(word, record.score); }); }); @@ -587,7 +588,7 @@ const Search = { break; // select one (max) score for the file. - const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); // add result to the result list results.push([ docNames[file], diff --git a/sphinx/theming.py b/sphinx/theming.py index 6d9986ba3c2..7f1c53d1ebc 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -6,12 +6,12 @@ import configparser import contextlib -import os import shutil import sys import tempfile +import tomllib from importlib.metadata import entry_points -from os import path +from pathlib import Path from typing import TYPE_CHECKING, Any from zipfile import ZipFile @@ -20,19 +20,12 @@ from sphinx.errors import ThemeError from sphinx.locale import __ from sphinx.util import logging +from sphinx.util._pathlib import _StrPath from sphinx.util.osutil import ensuredir -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - - if TYPE_CHECKING: from collections.abc import Callable - from typing import TypedDict - - from typing_extensions import Required + from typing import Required, TypedDict from sphinx.application import Sphinx @@ -69,8 +62,8 @@ def __init__( name: str, *, configs: dict[str, _ConfigFile], - paths: list[str], - tmp_dirs: list[str], + paths: list[Path], + tmp_dirs: list[Path], ) -> None: self.name = name self._dirs = tuple(paths) @@ -94,11 +87,11 @@ def __init__( self._options = options - def get_theme_dirs(self) -> list[str]: + def get_theme_dirs(self) -> list[_StrPath]: """Return a list of theme directories, beginning with this theme's, then the base theme's, then that one's base theme's, etc. """ - return list(self._dirs) + return list(map(_StrPath, self._dirs)) def get_config(self, section: str, name: str, default: Any = _NO_DEFAULT) -> Any: """Return the value for a theme configuration setting, searching the @@ -166,17 +159,17 @@ def __init__(self, app: Sphinx) -> None: def _load_builtin_themes(self) -> None: """Load built-in themes.""" - themes = self._find_themes(path.join(package_dir, 'themes')) + themes = self._find_themes(Path(package_dir, 'themes')) for name, theme in themes.items(): - self._themes[name] = theme + self._themes[name] = _StrPath(theme) def _load_additional_themes(self, theme_paths: list[str]) -> None: """Load additional themes placed at specified directories.""" for theme_path in theme_paths: - abs_theme_path = path.abspath(path.join(self._app.confdir, theme_path)) + abs_theme_path = (self._app.confdir / theme_path).resolve() themes = self._find_themes(abs_theme_path) for name, theme in themes.items(): - self._themes[name] = theme + self._themes[name] = _StrPath(theme) def _load_entry_point_themes(self) -> None: """Try to load a theme with the specified name. @@ -198,18 +191,17 @@ def _load_theme_closure( self._entry_point_themes[entry_point.name] = _load_theme_closure @staticmethod - def _find_themes(theme_path: str) -> dict[str, str]: + def _find_themes(theme_path: Path) -> dict[str, Path]: """Search themes from specified directory.""" - themes: dict[str, str] = {} - if not path.isdir(theme_path): + themes: dict[str, Path] = {} + if not theme_path.is_dir(): return themes - for entry in os.listdir(theme_path): - pathname = path.join(theme_path, entry) - if path.isfile(pathname) and entry.lower().endswith('.zip'): + for pathname in theme_path.iterdir(): + entry = pathname.name + if pathname.is_file() and pathname.suffix.lower() == '.zip': if _is_archived_theme(pathname): - name = entry[:-4] - themes[name] = pathname + themes[pathname.stem] = pathname else: logger.warning( __( @@ -219,9 +211,9 @@ def _find_themes(theme_path: str) -> dict[str, str]: entry, ) else: - toml_path = path.join(pathname, _THEME_TOML) - conf_path = path.join(pathname, _THEME_CONF) - if path.isfile(toml_path) or path.isfile(conf_path): + toml_path = pathname / _THEME_TOML + conf_path = pathname / _THEME_CONF + if toml_path.is_file() or conf_path.is_file(): themes[entry] = pathname return themes @@ -243,7 +235,7 @@ def create(self, name: str) -> Theme: return Theme(name, configs=themes, paths=theme_dirs, tmp_dirs=tmp_dirs) -def _is_archived_theme(filename: str, /) -> bool: +def _is_archived_theme(filename: Path, /) -> bool: """Check whether the specified file is an archived theme file or not.""" try: with ZipFile(filename) as f: @@ -255,13 +247,13 @@ def _is_archived_theme(filename: str, /) -> bool: def _load_theme_with_ancestors( name: str, - theme_paths: dict[str, str], + theme_paths: dict[str, _StrPath], entry_point_themes: dict[str, Callable[[], None]], /, -) -> tuple[dict[str, _ConfigFile], list[str], list[str]]: +) -> tuple[dict[str, _ConfigFile], list[Path], list[Path]]: themes: dict[str, _ConfigFile] = {} - theme_dirs: list[str] = [] - tmp_dirs: list[str] = [] + theme_dirs: list[Path] = [] + tmp_dirs: list[Path] = [] # having 10+ theme ancestors is ludicrous for _ in range(10): @@ -294,23 +286,23 @@ def _load_theme_with_ancestors( def _load_theme( - name: str, theme_path: str, / -) -> tuple[str, str, str | None, _ConfigFile]: - if path.isdir(theme_path): + name: str, theme_path: Path, / +) -> tuple[str, Path, Path | None, _ConfigFile]: + if theme_path.is_dir(): # already a directory, do nothing tmp_dir = None theme_dir = theme_path else: # extract the theme to a temp directory - tmp_dir = tempfile.mkdtemp('sxt') - theme_dir = path.join(tmp_dir, name) + tmp_dir = Path(tempfile.mkdtemp('sxt')) + theme_dir = tmp_dir / name _extract_zip(theme_path, theme_dir) - if path.isfile(toml_path := path.join(theme_dir, _THEME_TOML)): + if (toml_path := theme_dir / _THEME_TOML).is_file(): _cfg_table = _load_theme_toml(toml_path) inherit = _validate_theme_toml(_cfg_table, name) config = _convert_theme_toml(_cfg_table) - elif path.isfile(conf_path := path.join(theme_dir, _THEME_CONF)): + elif (conf_path := theme_dir / _THEME_CONF).is_file(): _cfg_parser = _load_theme_conf(conf_path) inherit = _validate_theme_conf(_cfg_parser, name) config = _convert_theme_conf(_cfg_parser) @@ -320,7 +312,7 @@ def _load_theme( return inherit, theme_dir, tmp_dir, config -def _extract_zip(filename: str, target_dir: str, /) -> None: +def _extract_zip(filename: Path, target_dir: Path, /) -> None: """Extract zip file to target directory.""" ensuredir(target_dir) @@ -328,16 +320,13 @@ def _extract_zip(filename: str, target_dir: str, /) -> None: for name in archive.namelist(): if name.endswith('/'): continue - entry = path.join(target_dir, name) - ensuredir(path.dirname(entry)) - with open(path.join(entry), 'wb') as fp: - fp.write(archive.read(name)) + entry = target_dir / name + ensuredir(entry.parent) + entry.write_bytes(archive.read(name)) -def _load_theme_toml(config_file_path: str, /) -> _ThemeToml: - with open(config_file_path, encoding='utf-8') as f: - config_text = f.read() - c = tomllib.loads(config_text) +def _load_theme_toml(config_file_path: Path, /) -> _ThemeToml: + c = tomllib.loads(config_file_path.read_text(encoding='utf-8')) return {s: c[s] for s in ('theme', 'options') if s in c} # type: ignore[return-value] @@ -388,7 +377,7 @@ def _convert_theme_toml(cfg: _ThemeToml, /) -> _ConfigFile: ) -def _load_theme_conf(config_file_path: str, /) -> configparser.RawConfigParser: +def _load_theme_conf(config_file_path: Path, /) -> configparser.RawConfigParser: c = configparser.RawConfigParser() c.read(config_file_path, encoding='utf-8') return c @@ -494,9 +483,9 @@ def _migrate_conf_to_toml(argv: list[str]) -> int: if len(argv) != 1: print('Usage: python -m sphinx.theming conf_to_toml ') # NoQA: T201 raise SystemExit(1) - theme_dir = path.realpath(argv[0]) - conf_path = path.join(theme_dir, _THEME_CONF) - if not path.isdir(theme_dir) or not path.isfile(conf_path): + theme_dir = Path(argv[0]).resolve() + conf_path = theme_dir / _THEME_CONF + if not theme_dir.is_dir() or not conf_path.is_file(): print( # NoQA: T201 f'{theme_dir!r} must be a path to a theme directory containing a "theme.conf" file' ) @@ -516,7 +505,7 @@ def _migrate_conf_to_toml(argv: list[str]) -> int: ] stylesheet = _cfg_parser.get('theme', 'stylesheet', fallback=...) - if stylesheet == '': + if not stylesheet: toml_lines.append('stylesheets = []') elif stylesheet is not ...: toml_lines.append('stylesheets = [') @@ -524,7 +513,7 @@ def _migrate_conf_to_toml(argv: list[str]) -> int: toml_lines.append(']') sidebar = _cfg_parser.get('theme', 'sidebars', fallback=...) - if sidebar == '': + if not sidebar: toml_lines.append('sidebars = []') elif sidebar is not ...: toml_lines.append('sidebars = [') @@ -550,9 +539,8 @@ def _migrate_conf_to_toml(argv: list[str]) -> int: if (d := default.replace('"', r'\"')) or True ] - toml_path = path.join(theme_dir, _THEME_TOML) - with open(toml_path, 'w', encoding='utf-8') as f: - f.write('\n'.join(toml_lines) + '\n') + toml_path = theme_dir / _THEME_TOML + toml_path.write_text('\n'.join(toml_lines) + '\n', encoding='utf-8') print(f'Written converted settings to {toml_path!r}') # NoQA: T201 return 0 diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 43449d57a88..2dd9fbc9280 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -420,7 +420,7 @@ def apply(self, **kwargs: Any) -> None: if not isinstance(node, LITERAL_TYPE_NODES): msgstr, _ = parse_noqa(msgstr) - if msgstr.strip() == '': + if not msgstr.strip(): # as-of-yet untranslated node['translated'] = False continue diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 07e28500c9e..e642a95b134 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -246,7 +246,7 @@ def matches_ignore(entry_type: str, entry_target: str) -> bool: return elif domain and typ in domain.dangling_warnings: msg = domain.dangling_warnings[typ] % {'target': target} - elif node.get('refdomain', 'std') not in ('', 'std'): + elif node.get('refdomain', 'std') not in {'', 'std'}: msg = __('%s:%s reference target not found: %s') % ( node['refdomain'], typ, diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index fd8556aa55c..7040952ac14 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -61,7 +61,7 @@ def handle(self, node: nodes.image) -> None: basename = os.path.basename(node['uri']) if '?' in basename: basename = basename.split('?')[0] - if basename == '' or len(basename) > MAX_FILENAME_LEN: + if not basename or len(basename) > MAX_FILENAME_LEN: filename, ext = os.path.splitext(node['uri']) basename = ( sha1(filename.encode(), usedforsecurity=False).hexdigest() + ext @@ -109,7 +109,7 @@ def _process_image(self, node: nodes.image, path: Path) -> None: self.app.env.original_image_uri[str_path] = node['uri'] mimetype = guess_mimetype(path, default='*') - if mimetype != '*' and path.suffix == '': + if mimetype != '*' and not path.suffix: # append a suffix if URI does not contain suffix ext = get_image_extension(mimetype) or '' with_ext = path.with_name(path.name + ext) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 07af8602079..73a66a9a60d 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -8,6 +8,7 @@ import re from typing import Any +from sphinx.errors import ExtensionError as _ExtensionError from sphinx.errors import FiletypeNotFoundError from sphinx.util import _files, _importer, logging from sphinx.util import index_entries as _index_entries @@ -86,6 +87,7 @@ def _sha1(data: bytes = b'', **_kw: Any) -> hashlib._Hash: 'sphinx.util.index_entries.split_into', (9, 0), ), + 'ExtensionError': (_ExtensionError, 'sphinx.errors.ExtensionError', (9, 0)), 'md5': (_md5, '', (9, 0)), 'sha1': (_sha1, '', (9, 0)), 'import_object': (_importer.import_object, '', (10, 0)), diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index c12ba5ca93e..127b1b247b5 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -90,7 +90,7 @@ def verify_description_mode(mode: str) -> None: - if mode not in ('lastIsName', 'noneIsName', 'markType', 'markName', 'param', 'udl'): + if mode not in {'lastIsName', 'noneIsName', 'markType', 'markName', 'param', 'udl'}: raise Exception("Description mode '%s' is invalid." % mode) @@ -108,6 +108,9 @@ def __eq__(self, other: object) -> bool: except AttributeError: return False + def __hash__(self) -> int: + return hash(sorted(self.__dict__.items())) + def clone(self) -> Any: return deepcopy(self) diff --git a/sphinx/util/console.py b/sphinx/util/console.py index 9a5a15a5bd9..86e4223782a 100644 --- a/sphinx/util/console.py +++ b/sphinx/util/console.py @@ -106,7 +106,7 @@ def color_terminal() -> bool: if 'COLORTERM' in os.environ: return True term = os.environ.get('TERM', 'dumb').lower() - return term in ('xterm', 'linux') or 'color' in term + return term in {'xterm', 'linux'} or 'color' in term def nocolor() -> None: diff --git a/sphinx/util/display.py b/sphinx/util/display.py index eebbb1a5133..95cb42bbfe8 100644 --- a/sphinx/util/display.py +++ b/sphinx/util/display.py @@ -9,9 +9,7 @@ if False: from collections.abc import Callable, Iterable, Iterator from types import TracebackType - from typing import Any, TypeVar - - from typing_extensions import ParamSpec + from typing import Any, ParamSpec, TypeVar T = TypeVar('T') P = ParamSpec('P') diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 87123768054..53e7620edc2 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -20,7 +20,7 @@ def separate_metadata(s: str | None) -> tuple[str | None, dict[str, str]]: return s, metadata for line in prepare_docstring(s): - if line.strip() == '': + if not line.strip(): in_other_element = False lines.append(line) else: diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 3a74040792e..a4c1c67098e 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -758,7 +758,7 @@ def new_document(source_path: str, settings: Any = None) -> nodes.document: caches the result of docutils' and use it on second call for instantiation. This makes an instantiation of document nodes much faster. """ - global __document_cache__ + global __document_cache__ # NoQA: PLW0603 try: cached_settings, reporter = __document_cache__ except NameError: diff --git a/sphinx/util/exceptions.py b/sphinx/util/exceptions.py index a0403c384d9..f12732558b5 100644 --- a/sphinx/util/exceptions.py +++ b/sphinx/util/exceptions.py @@ -41,7 +41,7 @@ def save_traceback(app: Sphinx | None, exc: BaseException) -> str: ) with NamedTemporaryFile( - 'w', suffix='.log', prefix='sphinx-err-', delete=False + 'w', encoding='utf-8', suffix='.log', prefix='sphinx-err-', delete=False ) as f: f.write(f"""\ # Platform: {sys.platform}; ({platform.platform()}) diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 5fca3c811b5..cd5619a729e 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -4,10 +4,9 @@ import os import re -import sys -from datetime import datetime, timezone +from datetime import datetime from os import path -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING import babel.dates from babel.messages.mofile import write_mo @@ -16,11 +15,11 @@ from sphinx.errors import SphinxError from sphinx.locale import __ from sphinx.util import logging +from sphinx.util._pathlib import _StrPath from sphinx.util.osutil import ( SEP, _last_modified_time, canon_path, - relpath, ) if TYPE_CHECKING: @@ -60,42 +59,41 @@ def __call__( # NoQA: E704 Formatter: TypeAlias = DateFormatter | TimeFormatter | DatetimeFormatter -if sys.version_info[:2] >= (3, 11): - from datetime import UTC -else: - UTC = timezone.utc +from datetime import UTC logger = logging.getLogger(__name__) -class LocaleFileInfoBase(NamedTuple): - base_dir: str - domain: str - charset: str +class CatalogInfo: + __slots__ = ('base_dir', 'domain', 'charset') + def __init__( + self, base_dir: str | os.PathLike[str], domain: str, charset: str + ) -> None: + self.base_dir = _StrPath(base_dir) + self.domain = domain + self.charset = charset -class CatalogInfo(LocaleFileInfoBase): @property def po_file(self) -> str: - return self.domain + '.po' + return f'{self.domain}.po' @property def mo_file(self) -> str: - return self.domain + '.mo' + return f'{self.domain}.mo' @property - def po_path(self) -> str: - return path.join(self.base_dir, self.po_file) + def po_path(self) -> _StrPath: + return self.base_dir / self.po_file @property - def mo_path(self) -> str: - return path.join(self.base_dir, self.mo_file) + def mo_path(self) -> _StrPath: + return self.base_dir / self.mo_file def is_outdated(self) -> bool: - return ( - not path.exists(self.mo_path) - or _last_modified_time(self.mo_path) < _last_modified_time(self.po_path) - ) # fmt: skip + return not self.mo_path.exists() or ( + _last_modified_time(self.mo_path) < _last_modified_time(self.po_path) + ) def write_mo(self, locale: str, use_fuzzy: bool = False) -> None: with open(self.po_path, encoding=self.charset) as file_po: @@ -122,37 +120,33 @@ def __init__( language: str, encoding: str, ) -> None: - self.basedir = basedir + self.basedir = _StrPath(basedir) self._locale_dirs = locale_dirs self.language = language self.encoding = encoding @property - def locale_dirs(self) -> Iterator[str]: + def locale_dirs(self) -> Iterator[_StrPath]: if not self.language: return for locale_dir in self._locale_dirs: - locale_dir = path.join(self.basedir, locale_dir) - locale_path = path.join(locale_dir, self.language, 'LC_MESSAGES') - if path.exists(locale_path): - yield locale_dir + locale_path = self.basedir / locale_dir / self.language / 'LC_MESSAGES' + if locale_path.exists(): + yield self.basedir / locale_dir else: logger.verbose(__('locale_dir %s does not exist'), locale_path) @property - def pofiles(self) -> Iterator[tuple[str, str]]: + def pofiles(self) -> Iterator[tuple[_StrPath, _StrPath]]: for locale_dir in self.locale_dirs: - basedir = path.join(locale_dir, self.language, 'LC_MESSAGES') - for root, dirnames, filenames in os.walk(basedir): + locale_path = locale_dir / self.language / 'LC_MESSAGES' + for abs_path in locale_path.rglob('*.po'): + rel_path = abs_path.relative_to(locale_path) # skip dot-directories - for dirname in [d for d in dirnames if d.startswith('.')]: - dirnames.remove(dirname) - - for filename in filenames: - if filename.endswith('.po'): - fullpath = path.join(root, filename) - yield basedir, relpath(fullpath, basedir) + if any(part.startswith('.') for part in rel_path.parts[:-1]): + continue + yield locale_path, rel_path @property def catalogs(self) -> Iterator[CatalogInfo]: diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 194b79db527..c925485e11a 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -24,7 +24,7 @@ from sphinx.util.typing import stringify_annotation if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Callable, Iterator, Sequence from inspect import _ParameterKind from types import MethodType, ModuleType from typing import Final, Protocol, TypeAlias @@ -523,6 +523,9 @@ def __init__(self, value: str) -> None: def __eq__(self, other: object) -> bool: return self.value == other + def __hash__(self) -> int: + return hash(self.value) + def __repr__(self) -> str: return self.value @@ -547,7 +550,7 @@ def __hash__(self) -> int: return hash(self.name) def __repr__(self) -> str: - return self.name + return f'{self.__class__.__name__}({self.name!r})' class TypeAliasModule: @@ -583,7 +586,7 @@ def __getattr__(self, name: str) -> Any: return getattr(self.__module, name) -class TypeAliasNamespace(dict[str, Any]): +class TypeAliasNamespace(Mapping[str, Any]): """Pseudo namespace class for :confval:`autodoc_type_aliases`. Useful for looking up nested objects via ``namespace.foo.bar.Class``. @@ -593,7 +596,9 @@ def __init__(self, mapping: Mapping[str, str]) -> None: super().__init__() self.__mapping = mapping - def __getitem__(self, key: str) -> Any: + def __getitem__(self, key: object) -> Any: + if not isinstance(key, str): + raise KeyError if key in self.__mapping: # exactly matched return TypeAliasForwardRef(self.__mapping[key]) @@ -606,6 +611,22 @@ def __getitem__(self, key: str) -> Any: else: raise KeyError + def __contains__(self, key: object) -> bool: + if not isinstance(key, str): + return False + ns = self.__mapping + prefix = f'{key}.' + return key in ns or any(k.startswith(prefix) for k in ns) + + def __iter__(self) -> Iterator[str]: + for k in self.__mapping: + yield k + for i in range(k.count('.')): + yield k.rsplit('.', i + 1)[0] + + def __len__(self) -> int: + return sum(k.count('.') + 1 for k in self.__mapping) + def _should_unwrap(subject: _SignatureType) -> bool: """Check the function should be unwrapped on getting signature.""" @@ -709,14 +730,20 @@ def _evaluate_forwardref( localns: dict[str, Any] | None, ) -> Any: """Evaluate a forward reference.""" + if sys.version_info[:2] >= (3, 14): + # https://docs.python.org/dev/library/annotationlib.html#annotationlib.ForwardRef.evaluate + # https://docs.python.org/dev/library/typing.html#typing.evaluate_forward_ref + return typing.evaluate_forward_ref(ref, globals=globalns, locals=localns) if sys.version_info >= (3, 12, 4): # ``type_params`` were added in 3.13 and the signature of _evaluate() # is not backward-compatible (it was backported to 3.12.4, so anything # before 3.12.4 still has the old signature). # # See: https://github.com/python/cpython/pull/118104. - return ref._evaluate(globalns, localns, {}, recursive_guard=frozenset()) # type: ignore[arg-type, misc] - return ref._evaluate(globalns, localns, frozenset()) + return ref._evaluate( + globalns, localns, type_params=(), recursive_guard=frozenset() + ) # type: ignore[call-arg] + return ref._evaluate(globalns, localns, recursive_guard=frozenset()) def _evaluate( @@ -727,14 +754,14 @@ def _evaluate( """Evaluate unresolved type annotation.""" try: if isinstance(annotation, str): - ref = ForwardRef(annotation, True) + ref = ForwardRef(annotation) annotation = _evaluate_forwardref(ref, globalns, localns) if isinstance(annotation, ForwardRef): annotation = _evaluate_forwardref(ref, globalns, localns) elif isinstance(annotation, str): # might be a ForwardRef'ed annotation in overloaded functions - ref = ForwardRef(annotation, True) + ref = ForwardRef(annotation) annotation = _evaluate_forwardref(ref, globalns, localns) except (NameError, TypeError): # failed to evaluate type. skipped. @@ -772,11 +799,11 @@ def stringify_signature( ): # PEP-570: Separator for Positional Only Parameter: / args.append('/') - if param.kind == Parameter.KEYWORD_ONLY and last_kind in ( + if param.kind == Parameter.KEYWORD_ONLY and last_kind in { Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY, None, - ): + }: # PEP-3102: Separator for Keyword Only Parameter: * args.append('*') diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py index ab6c5ba7e7b..507d7a1d8ed 100644 --- a/sphinx/util/inventory.py +++ b/sphinx/util/inventory.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os import re import zlib from typing import TYPE_CHECKING @@ -14,6 +13,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: + import os from collections.abc import Callable, Iterator from sphinx.builders import Builder @@ -193,14 +193,14 @@ def load_v2( @classmethod def dump( cls: type[InventoryFile], - filename: str, + filename: str | os.PathLike[str], env: BuildEnvironment, builder: Builder, ) -> None: def escape(string: str) -> str: return re.sub('\\s+', ' ', string) - with open(os.path.join(filename), 'wb') as f: + with open(filename, 'wb') as f: # header f.write( ( diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index d8f2b82071c..7f06ae194fc 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -593,7 +593,7 @@ def make_id( node_id = None elif term: node_id = _make_id(term) - if node_id == '': + if not node_id: node_id = None # fallback to None while node_id is None or node_id in document.ids: diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 64f07a43249..4e092692bf0 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -17,7 +17,6 @@ from sphinx.locale import __ if TYPE_CHECKING: - from types import TracebackType from typing import Any # SEP separates path elements in the canonical file names @@ -134,10 +133,14 @@ def copyfile( logger.warning(msg, source, dest, type='misc', subtype='copy_overwrite') return - shutil.copyfile(source, dest) - with contextlib.suppress(OSError): - # don't do full copystat because the source may be read-only - _copy_times(source, dest) + if sys.platform == 'win32': + # copy2() uses Windows API calls + shutil.copy2(source, dest) + else: + shutil.copyfile(source, dest) + with contextlib.suppress(OSError): + # don't do full copystat because the source may be read-only + _copy_times(source, dest) _no_fn_re = re.compile(r'[^a-zA-Z0-9_-]') @@ -173,31 +176,6 @@ def relpath( abspath = path.abspath -class _chdir: - """Remove this fall-back once support for Python 3.10 is removed.""" - - def __init__(self, target_dir: str, /) -> None: - self.path = target_dir - self._dirs: list[str] = [] - - def __enter__(self) -> None: - self._dirs.append(os.getcwd()) - os.chdir(self.path) - - def __exit__( - self, - type: type[BaseException] | None, - value: BaseException | None, - traceback: TracebackType | None, - /, - ) -> None: - os.chdir(self._dirs.pop()) - - -if sys.version_info[:2] < (3, 11): - cd = _chdir - - class FileAvoidWrite: """File-like object that buffers output and only writes if content changed. diff --git a/sphinx/util/texescape.py b/sphinx/util/texescape.py index 57d35450e60..ba5acf4f9aa 100644 --- a/sphinx/util/texescape.py +++ b/sphinx/util/texescape.py @@ -103,7 +103,7 @@ def escape(s: str, latex_engine: str | None = None) -> str: """Escape text for LaTeX output.""" - if latex_engine in ('lualatex', 'xelatex'): + if latex_engine in {'lualatex', 'xelatex'}: # unicode based LaTeX engine return s.translate(_tex_escape_map_without_unicode) else: @@ -112,7 +112,7 @@ def escape(s: str, latex_engine: str | None = None) -> str: def hlescape(s: str, latex_engine: str | None = None) -> str: """Escape text for LaTeX highlighter.""" - if latex_engine in ('lualatex', 'xelatex'): + if latex_engine in {'lualatex', 'xelatex'}: # unicode based LaTeX engine return s.translate(_tex_hlescape_map_without_unicode) else: diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 35d444aa583..2e8d9e689f7 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -18,6 +18,7 @@ TypedDict, TypeVar, Union, + Unpack, ) from docutils import nodes @@ -121,8 +122,7 @@ def __call__( # NoQA: E704 # Readable file stream for inventory loading if TYPE_CHECKING: from types import TracebackType - - from typing_extensions import Self + from typing import Self _T_co = TypeVar('_T_co', str, bytes, covariant=True) @@ -221,19 +221,7 @@ def _is_annotated_form(obj: Any) -> TypeIs[Annotated[Any, ...]]: def _is_unpack_form(obj: Any) -> bool: """Check if the object is :class:`typing.Unpack` or equivalent.""" - if sys.version_info >= (3, 11): - from typing import Unpack - - # typing_extensions.Unpack != typing.Unpack for 3.11, but we assume - # that typing_extensions.Unpack should not be used in that case - return typing.get_origin(obj) is Unpack - - # Python 3.10 requires typing_extensions.Unpack - origin = typing.get_origin(obj) - return ( - getattr(origin, '__module__', None) == 'typing_extensions' - and origin.__name__ == 'Unpack' - ) + return typing.get_origin(obj) is Unpack def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: @@ -307,11 +295,14 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s ) elif isinstance(cls, NewType): return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] - elif isinstance(cls, types.UnionType): + elif isinstance(cls, types.UnionType) or ( + isgenericalias(cls) and cls_module_is_typing and cls.__origin__ is Union + ): # Union types (PEP 585) retain their definition order when they # are printed natively and ``None``-like types are kept as is. + # *cls* is defined in ``typing``, and thus ``__args__`` must exist return ' | '.join(restify(a, mode) for a in cls.__args__) - elif cls.__module__ in ('__builtin__', 'builtins'): + elif cls.__module__ in {'__builtin__', 'builtins'}: if hasattr(cls, '__args__'): if not cls.__args__: # Empty tuple, list, ... return rf':py:class:`{cls.__name__}`\ [{cls.__args__!r}]' @@ -321,15 +312,11 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s ) return rf':py:class:`{cls.__name__}`\ [{concatenated_args}]' return f':py:class:`{cls.__name__}`' - elif isgenericalias(cls) and cls_module_is_typing and cls.__origin__ is Union: - # *cls* is defined in ``typing``, and thus ``__args__`` must exist - return ' | '.join(restify(a, mode) for a in cls.__args__) elif isgenericalias(cls): - if isinstance(cls.__origin__, typing._SpecialForm): - # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard; TypeIs - # Required/NotRequired - text = restify(cls.__origin__, mode) - elif cls.__name__: + if cls.__name__ and not isinstance(cls.__origin__, typing._SpecialForm): + # Represent generic aliases as the classes in ``typing`` rather + # than the underlying aliased classes, + # e.g. ``~typing.Tuple`` instead of ``tuple``. text = f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' else: text = restify(cls.__origin__, mode) @@ -361,7 +348,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return rf'{text}\ [{args}]' elif isinstance(cls, typing._SpecialForm): return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] - elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: + elif cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' elif hasattr(cls, '__qualname__'): @@ -437,6 +424,9 @@ def stringify_annotation( annotation_module: str = getattr(annotation, '__module__', '') annotation_name: str = getattr(annotation, '__name__', '') annotation_module_is_typing = annotation_module == 'typing' + if sys.version_info[:2] >= (3, 14) and isinstance(annotation, ForwardRef): + # ForwardRef moved from `typing` to `annotationlib` in Python 3.14. + annotation_module_is_typing = True # Extract the annotation's base type by considering formattable cases if isinstance(annotation, TypeVar) and not _is_unpack_form(annotation): diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index b87a45662a0..e002788fc97 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -820,7 +820,7 @@ def visit_Text(self, node: Text) -> None: if token.strip(): # protect literal text from line wrapping self.body.append('%s' % token) - elif token in ' \n': + elif token in {' ', '\n'}: # allow breaks at whitespace self.body.append(token) else: diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 8aa24fdda90..c98135efa7f 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -291,7 +291,7 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str: amount, unit = match.groups()[:2] if scale == 100: float(amount) # validate amount is float - if unit in ('', 'px'): + if unit in {'', 'px'}: res = r'%s\sphinxpxdimen' % amount elif unit == 'pt': res = '%sbp' % amount # convert to 'bp' @@ -299,7 +299,7 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str: res = r'%.3f\linewidth' % (float(amount) / 100.0) else: amount_float = float(amount) * scale / 100.0 - if unit in ('', 'px'): + if unit in {'', 'px'}: res = r'%.5f\sphinxpxdimen' % amount_float elif unit == 'pt': res = '%.5fbp' % amount_float @@ -1090,7 +1090,7 @@ def depart_seealso(self, node: Element) -> None: self.no_latex_floats -= 1 def visit_rubric(self, node: nodes.rubric) -> None: - if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')): + if len(node) == 1 and node.astext() in {'Footnotes', _('Footnotes')}: raise nodes.SkipNode tag = 'subsubsection' if 'heading-level' in node: @@ -1337,7 +1337,7 @@ def visit_entry(self, node: Element) -> None: if ( len(node) == 1 and isinstance(node[0], nodes.paragraph) - and node.astext() == '' + and not node.astext() ): pass else: @@ -1684,7 +1684,7 @@ def visit_figure(self, node: Element) -> None: if any(isinstance(child, nodes.caption) for child in node): self.body.append(r'\capstart') self.context.append(r'\end{sphinxfigure-in-table}\relax' + CR) - elif node.get('align', '') in ('left', 'right'): + elif node.get('align', '') in {'left', 'right'}: length = None if 'width' in node: length = self.latex_image_length(node['width']) diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index e7173febbfa..5bfc23481c0 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -259,7 +259,7 @@ def visit_footnote(self, node: Element) -> None: # overwritten -- handle footnotes rubric def visit_rubric(self, node: Element) -> None: self.ensure_eol() - if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')): + if len(node) == 1 and node.astext() in {'Footnotes', _('Footnotes')}: self.body.append('.SH ' + self.deunicode(node.astext()).upper() + '\n') raise nodes.SkipNode self.body.append('.sp\n') diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 2718a3aee91..997561b16fd 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -240,7 +240,7 @@ def init_settings(self) -> None: # filename if not elements['filename']: elements['filename'] = self.document.get('source') or 'untitled' - if elements['filename'][-4:] in ('.txt', '.rst'): # type: ignore[index] + if elements['filename'][-4:] in {'.txt', '.rst'}: # type: ignore[index] elements['filename'] = elements['filename'][:-4] # type: ignore[index] elements['filename'] += '.info' # type: ignore[operator] # direntry @@ -657,7 +657,7 @@ def depart_title(self, node: Element) -> None: self.body.append('\n\n') def visit_rubric(self, node: Element) -> None: - if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')): + if len(node) == 1 and node.astext() in {'Footnotes', _('Footnotes')}: raise nodes.SkipNode try: rubric = self.rubrics[self.section_level] diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index efe3c46c5c0..c16181ec247 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -45,7 +45,7 @@ def __hash__(self) -> int: return hash((self.col, self.row)) def __bool__(self) -> bool: - return self.text != '' and self.col is not None and self.row is not None + return bool(self.text) and self.col is not None and self.row is not None def wrap(self, width: int) -> None: self.wrapped = my_wrap(self.text, width) @@ -292,7 +292,7 @@ def _wrap_chunks(self, chunks: list[str]) -> list[str]: width = self.width - column_width(indent) - if self.drop_whitespace and chunks[-1].strip() == '' and lines: + if self.drop_whitespace and not chunks[-1].strip() and lines: del chunks[-1] while chunks: @@ -308,7 +308,7 @@ def _wrap_chunks(self, chunks: list[str]) -> list[str]: if chunks and column_width(chunks[-1]) > width: self._handle_long_word(chunks, cur_line, cur_len, width) - if self.drop_whitespace and cur_line and cur_line[-1].strip() == '': + if self.drop_whitespace and cur_line and not cur_line[-1].strip(): del cur_line[-1] if cur_line: diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py index 695169a092b..825f6da5ca7 100644 --- a/sphinx/writers/xml.py +++ b/sphinx/writers/xml.py @@ -17,18 +17,18 @@ def __init__(self, builder: Builder) -> None: super().__init__() self.builder = builder - # A lambda function to generate translator lazily - self.translator_class = lambda document: self.builder.create_translator( - document - ) - def translate(self, *args: Any, **kwargs: Any) -> None: self.document.settings.newlines = self.document.settings.indents = ( self.builder.env.config.xml_pretty ) self.document.settings.xml_declaration = True self.document.settings.doctype_declaration = True - return super().translate() + + # copied from docutils.writers.docutils_xml.Writer.translate() + # so that we can override the translator class + self.visitor = visitor = self.builder.create_translator(self.document) + self.document.walkabout(visitor) + self.output = ''.join(visitor.output) # type: ignore[attr-defined] class PseudoXMLWriter(BaseXMLWriter): # type: ignore[misc] diff --git a/tests/conftest.py b/tests/conftest.py index bf8b3eb2ebe..8501825a025 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ def _init_console( - locale_dir: str | None = sphinx.locale._LOCALE_DIR, + locale_dir: str | os.PathLike[str] | None = sphinx.locale._LOCALE_DIR, catalog: str = 'sphinx', ) -> tuple[gettext.NullTranslations, bool]: """Monkeypatch ``init_console`` to skip its action. diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js index 989c877a8cc..e5837e65d56 100644 --- a/tests/js/fixtures/cpp/searchindex.js +++ b/tests/js/fixtures/cpp/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"sphinx (c++ class)": [[0, "_CPPv46Sphinx", false]]}, "objects": {"": [[0, 0, 1, "_CPPv46Sphinx", "Sphinx"]]}, "objnames": {"0": ["cpp", "class", "C++ class"]}, "objtypes": {"0": "cpp:class"}, "terms": {"The": 0, "becaus": 0, "c": 0, "can": 0, "cardin": 0, "challeng": 0, "charact": 0, "class": 0, "descript": 0, "drop": 0, "engin": 0, "fixtur": 0, "frequent": 0, "gener": 0, "i": 0, "index": 0, "inflat": 0, "mathemat": 0, "occur": 0, "often": 0, "project": 0, "punctuat": 0, "queri": 0, "relat": 0, "sampl": 0, "search": 0, "size": 0, "sphinx": 0, "term": 0, "thei": 0, "thi": 0, "token": 0, "us": 0, "web": 0, "would": 0}, "titles": ["<no title>"], "titleterms": {}}) \ No newline at end of file +Search.setIndex({"alltitles":{},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"gener":0,"i":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"thei":0,"thi":0,"token":0,"us":0,"web":0,"would":0},"titles":["<no title>"],"titleterms":{}}) \ No newline at end of file diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js index c4e4a32e30a..b3e2977792c 100644 --- a/tests/js/fixtures/multiterm/searchindex.js +++ b/tests/js/fixtures/multiterm/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"Main Page": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"At": 0, "adjac": 0, "all": 0, "an": 0, "appear": 0, "applic": 0, "ar": 0, "built": 0, "can": 0, "check": 0, "contain": 0, "do": 0, "document": 0, "doesn": 0, "each": 0, "fixtur": 0, "format": 0, "function": 0, "futur": 0, "html": 0, "i": 0, "includ": 0, "match": 0, "messag": 0, "multipl": 0, "multiterm": 0, "order": 0, "other": 0, "output": 0, "perform": 0, "perhap": 0, "phrase": 0, "project": 0, "queri": 0, "requir": 0, "same": 0, "search": 0, "successfulli": 0, "support": 0, "t": 0, "term": 0, "test": 0, "thi": 0, "time": 0, "us": 0, "when": 0, "write": 0}, "titles": ["Main Page"], "titleterms": {"main": 0, "page": 0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"adjac":0,"all":0,"an":0,"appear":0,"applic":0,"ar":0,"built":0,"can":0,"check":0,"contain":0,"do":0,"document":0,"doesn":0,"each":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"i":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"other":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"same":0,"search":0,"successfulli":0,"support":0,"t":0,"term":0,"test":0,"thi":0,"time":0,"us":0,"when":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}}) \ No newline at end of file diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js index 4925083169b..ac024bf0c6e 100644 --- a/tests/js/fixtures/partial/searchindex.js +++ b/tests/js/fixtures/partial/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"sphinx_utils module": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"ar": 0, "both": 0, "built": 0, "confirm": 0, "document": 0, "function": 0, "html": 0, "i": 0, "includ": 0, "input": 0, "javascript": 0, "match": 0, "partial": 0, "possibl": 0, "project": 0, "provid": 0, "restructuredtext": 0, "sampl": 0, "search": 0, "should": 0, "term": 0, "thi": 0, "titl": 0, "us": 0, "when": 0}, "titles": ["sphinx_utils module"], "titleterms": {"modul": 0, "sphinx_util": 0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"ar":0,"both":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"i":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"should":0,"term":0,"thi":0,"titl":0,"us":0,"when":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}}) \ No newline at end of file diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js index 293b087149b..987be77992a 100644 --- a/tests/js/fixtures/titles/searchindex.js +++ b/tests/js/fixtures/titles/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"Main Page": [[0, null]], "Relevance": [[0, "relevance"], [1, null]], "Result Scoring": [[0, "result-scoring"]]}, "docnames": ["index", "relevance"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "relevance.rst"], "indexentries": {"example (class in relevance)": [[0, "relevance.Example", false]], "module": [[0, "module-relevance", false]], "relevance": [[0, "index-1", false], [0, "module-relevance", false]], "relevance (relevance.example attribute)": [[0, "relevance.Example.relevance", false]], "scoring": [[0, "index-0", true]]}, "objects": {"": [[0, 0, 0, "-", "relevance"]], "relevance": [[0, 1, 1, "", "Example"]], "relevance.Example": [[0, 2, 1, "", "relevance"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute"}, "terms": {"": [0, 1], "A": 1, "By": 0, "For": [0, 1], "In": [0, 1], "against": 0, "align": 0, "also": 1, "an": 0, "answer": 0, "appear": 1, "ar": 1, "area": 0, "ask": 0, "assign": 0, "attempt": 0, "attribut": 0, "both": 0, "built": 1, "can": [0, 1], "class": 0, "code": [0, 1], "collect": 0, "consid": 1, "contain": 0, "context": 0, "corpu": 1, "could": 1, "demonstr": 0, "describ": 1, "detail": 1, "determin": [0, 1], "docstr": 0, "document": [0, 1], "domain": 1, "dure": 0, "engin": 0, "evalu": 0, "exampl": [0, 1], "extract": 0, "feedback": 0, "find": 0, "found": 0, "from": 0, "function": 1, "ha": 1, "handl": 0, "happen": 1, "head": 0, "help": 0, "highli": [0, 1], "how": 0, "i": [0, 1], "improv": 0, "inform": 0, "intend": 0, "issu": [0, 1], "itself": 1, "knowledg": 0, "languag": 1, "less": 1, "like": [0, 1], "mani": 0, "match": 0, "mention": 1, "more": 0, "name": [0, 1], "numer": 0, "object": 0, "often": 0, "one": [0, 1], "onli": [0, 1], "order": 0, "other": 0, "over": 0, "page": 1, "part": 1, "particular": 0, "present": 0, "printf": 1, "program": 1, "project": 0, "queri": [0, 1], "question": 0, "re": 0, "rel": 0, "research": 0, "result": 1, "retriev": 0, "sai": 0, "same": 1, "search": [0, 1], "seem": 0, "softwar": 1, "some": 1, "sphinx": 0, "straightforward": 1, "subject": 0, "subsect": 0, "term": [0, 1], "test": 0, "text": 0, "than": [0, 1], "thei": 0, "them": 0, "thi": 0, "time": 0, "titl": 0, "two": 0, "typic": 0, "us": 0, "user": [0, 1], "we": [0, 1], "when": 0, "whether": 1, "which": 0, "within": 0, "word": 0, "would": [0, 1]}, "titles": ["Main Page", "Relevance"], "titleterms": {"main": 0, "page": 0, "relev": [0, 1], "result": 0, "score": 0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"":[0,1],"A":1,"By":0,"For":[0,1],"In":[0,1],"against":0,"align":0,"also":1,"an":0,"answer":0,"appear":1,"ar":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"both":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpu":1,"could":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"from":0,"function":1,"ha":1,"handl":0,"happen":1,"head":0,"help":0,"highli":[0,1],"how":0,"i":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"itself":1,"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"more":0,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"other":0,"over":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"rel":0,"research":0,"result":1,"retriev":0,"sai":0,"same":1,"search":[0,1],"seem":0,"softwar":1,"some":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"than":[0,1],"thei":0,"them":0,"thi":0,"time":0,"titl":0,"two":0,"typic":0,"us":0,"user":[0,1],"we":[0,1],"when":0,"whether":1,"which":0,"within":0,"word":0,"would":[0,1]},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}}) \ No newline at end of file diff --git a/tests/roots/test-apidoc-toc/mypackage/main.py b/tests/roots/test-apidoc-toc/mypackage/main.py index f532813a722..1f6d1376cbb 100755 --- a/tests/roots/test-apidoc-toc/mypackage/main.py +++ b/tests/roots/test-apidoc-toc/mypackage/main.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import os +from pathlib import Path import mod_resource import mod_something @@ -8,8 +8,6 @@ if __name__ == "__main__": print(f"Hello, world! -> something returns: {mod_something.something()}") - res_path = \ - os.path.join(os.path.dirname(mod_resource.__file__), 'resource.txt') - with open(res_path, encoding='utf-8') as f: - text = f.read() + res_path = Path(mod_resource.__file__).parent / 'resource.txt' + text = res_path.read_text(encoding='utf-8') print(f"From mod_resource:resource.txt -> {text}") diff --git a/tests/roots/test-theming/test_theme/__init__.py b/tests/roots/test-theming/test_theme/__init__.py index 13bdc4b2c13..e69de29bb2d 100644 --- a/tests/roots/test-theming/test_theme/__init__.py +++ b/tests/roots/test-theming/test_theme/__init__.py @@ -1,5 +0,0 @@ -import os - - -def get_path(): - return os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/test_builders/test_build_epub.py b/tests/test_builders/test_build_epub.py index 2dbdacf852b..3c2be6323d4 100644 --- a/tests/test_builders/test_build_epub.py +++ b/tests/test_builders/test_build_epub.py @@ -450,13 +450,13 @@ def test_run_epubcheck(app): if not runnable(['java', '-version']): pytest.skip('Unable to run Java; skipping test') - epubcheck = os.environ.get('EPUBCHECK_PATH', '/usr/share/java/epubcheck.jar') - if not os.path.exists(epubcheck): + epubcheck = Path(os.environ.get('EPUBCHECK_PATH', '/usr/share/java/epubcheck.jar')) + if not epubcheck.exists(): pytest.skip('Could not find epubcheck; skipping test') try: subprocess.run( - ['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'], + ['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'], # NoQA: S607 capture_output=True, check=True, ) diff --git a/tests/test_builders/test_build_gettext.py b/tests/test_builders/test_build_gettext.py index 169cc3bf438..00d7f826ecc 100644 --- a/tests/test_builders/test_build_gettext.py +++ b/tests/test_builders/test_build_gettext.py @@ -1,21 +1,16 @@ """Test the build process with gettext builder with the test root.""" import gettext -import os import re import subprocess -import sys +from contextlib import chdir +from pathlib import Path from subprocess import CalledProcessError import pytest from sphinx.builders.gettext import Catalog, MsgOrigin -if sys.version_info[:2] >= (3, 11): - from contextlib import chdir -else: - from sphinx.util.osutil import _chdir as chdir - _MSGID_PATTERN = re.compile(r'msgid "((?:\n|.)*?)"\nmsgstr', re.MULTILINE) @@ -99,7 +94,7 @@ def test_msgfmt(app): 'msgfmt', 'en_US.po', '-o', - os.path.join('en', 'LC_MESSAGES', 'test_root.mo'), + Path('en', 'LC_MESSAGES', 'test_root.mo'), ] subprocess.run(args, capture_output=True, check=True) except OSError: diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 2c4601849c9..1e82e70eaa1 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -10,7 +10,6 @@ import pytest from sphinx.builders.html import validate_html_extra_path, validate_html_static_path -from sphinx.deprecation import RemovedInSphinx90Warning from sphinx.errors import ConfigError from sphinx.util.console import strip_colors from sphinx.util.inventory import InventoryFile @@ -512,12 +511,16 @@ def test_validate_html_extra_path(app): app.outdir, # outdir app.outdir / '_static', # inside outdir ] # fmt: skip - with pytest.warns( - RemovedInSphinx90Warning, match='Use "pathlib.Path" or "os.fspath" instead' - ): - validate_html_extra_path(app, app.config) + + validate_html_extra_path(app, app.config) assert app.config.html_extra_path == ['_static'] + warnings = strip_colors(app.warning.getvalue()).splitlines() + assert "html_extra_path entry '/path/to/not_found' does not exist" in warnings[0] + assert warnings[1].endswith(' is placed inside outdir') + assert warnings[2].endswith(' does not exist') + assert len(warnings) == 3 + @pytest.mark.sphinx( 'html', @@ -532,12 +535,16 @@ def test_validate_html_static_path(app): app.outdir, # outdir app.outdir / '_static', # inside outdir ] # fmt: skip - with pytest.warns( - RemovedInSphinx90Warning, match='Use "pathlib.Path" or "os.fspath" instead' - ): - validate_html_static_path(app, app.config) + + validate_html_static_path(app, app.config) assert app.config.html_static_path == ['_static'] + warnings = strip_colors(app.warning.getvalue()).splitlines() + assert "html_static_path entry '/path/to/not_found' does not exist" in warnings[0] + assert warnings[1].endswith(' is placed inside outdir') + assert warnings[2].endswith(' does not exist') + assert len(warnings) == 3 + @pytest.mark.sphinx( 'html', diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index ea6dc7475ca..b6207bae60c 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -4,6 +4,7 @@ import os import re import subprocess +from contextlib import chdir from pathlib import Path from shutil import copyfile from subprocess import CalledProcessError @@ -20,11 +21,6 @@ from tests.utils import http_server -try: - from contextlib import chdir -except ImportError: - from sphinx.util.osutil import _chdir as chdir - STYLEFILES = [ 'article.cls', 'fancyhdr.sty', @@ -44,7 +40,7 @@ # only run latex if all needed packages are there def kpsetest(*filenames): try: - subprocess.run(['kpsewhich', *list(filenames)], capture_output=True, check=True) + subprocess.run(['kpsewhich', *list(filenames)], capture_output=True, check=True) # NoQA: S607 return True except (OSError, CalledProcessError): return False # command not found or exit with non-zero @@ -2193,7 +2189,7 @@ def test_duplicated_labels_before_module(app): tested_labels = set() # iterate over the (explicit) labels in the corresponding index.rst - for rst_label_name in [ + for rst_label_name in ( 'label_1a', 'label_1b', 'label_2', @@ -2202,7 +2198,7 @@ def test_duplicated_labels_before_module(app): 'label_auto_1b', 'label_auto_2', 'label_auto_3', - ]: + ): tex_label_name = 'index:' + rst_label_name.replace('_', '-') tex_label_code = r'\phantomsection\label{\detokenize{%s}}' % tex_label_name assert ( diff --git a/tests/test_command_line.py b/tests/test_command_line.py index 63fb9e036d3..e346784b5dd 100644 --- a/tests/test_command_line.py +++ b/tests/test_command_line.py @@ -1,7 +1,7 @@ from __future__ import annotations -import os.path import sys +from pathlib import Path from typing import Any import pytest @@ -52,8 +52,8 @@ EXPECTED_MAKE_MODE = { 'builder': 'html', 'sourcedir': 'source_dir', - 'outputdir': os.path.join('build_dir', 'html'), - 'doctreedir': os.path.join('build_dir', 'doctrees'), + 'outputdir': str(Path('build_dir', 'html')), + 'doctreedir': str(Path('build_dir', 'doctrees')), 'filenames': ['filename1', 'filename2'], 'freshenv': True, 'noconfig': True, diff --git a/tests/test_directives/test_directive_code.py b/tests/test_directives/test_directive_code.py index 65e16b805bd..dacf8a3b334 100644 --- a/tests/test_directives/test_directive_code.py +++ b/tests/test_directives/test_directive_code.py @@ -1,7 +1,5 @@ """Test the code-block directive.""" -import os.path - import pytest from docutils import nodes @@ -257,12 +255,13 @@ def test_LiteralIncludeReader_tabwidth_dedent(testroot): def test_LiteralIncludeReader_diff(testroot, literal_inc_path): - options = {'diff': testroot / 'literal-diff.inc'} + literal_diff_path = testroot / 'literal-diff.inc' + options = {'diff': literal_diff_path} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) content, lines = reader.read() assert content == ( - '--- ' + os.path.join(testroot, 'literal-diff.inc') + '\n' - '+++ ' + os.path.join(testroot, 'literal.inc') + '\n' + f'--- {literal_diff_path}\n' + f'+++ {literal_inc_path}\n' '@@ -6,8 +6,8 @@\n' ' pass\n' ' \n' diff --git a/tests/test_directives/test_directive_only.py b/tests/test_directives/test_directive_only.py index f4e205036b6..de9230c04da 100644 --- a/tests/test_directives/test_directive_only.py +++ b/tests/test_directives/test_directive_only.py @@ -37,10 +37,7 @@ def testsects(prefix, sects, indent=0): doctree = app.env.get_doctree('only') app.env.apply_post_transforms(doctree, 'only') - parts = [ - getsects(n) - for n in [_n for _n in doctree.children if isinstance(_n, nodes.section)] - ] + parts = [getsects(n) for n in doctree.children if isinstance(n, nodes.section)] for i, s in enumerate(parts): testsects(str(i + 1) + '.', s, 4) actual_headings = '\n'.join(p[0] for p in parts) diff --git a/tests/test_domains/test_domain_c.py b/tests/test_domains/test_domain_c.py index 491c0844b69..81451f29845 100644 --- a/tests/test_domains/test_domain_c.py +++ b/tests/test_domains/test_domain_c.py @@ -48,7 +48,7 @@ def _check(name, input, idDict, output, key, asTextOutput): if key is None: key = name key += ' ' - if name in ('function', 'member'): + if name in {'function', 'member'}: inputActual = input outputAst = output outputAsText = output @@ -192,8 +192,8 @@ def exprCheck(expr, output=None): exprCheck(expr) expr = i + l + u exprCheck(expr) - for suffix in ['', 'f', 'F', 'l', 'L']: - for e in [ + for suffix in ('', 'f', 'F', 'l', 'L'): + for e in ( '5e42', '5e+42', '5e-42', @@ -213,10 +213,10 @@ def exprCheck(expr, output=None): "1'2'3.e7'8'9", ".4'5'6e7'8'9", "1'2'3.4'5'6e7'8'9", - ]: + ): expr = e + suffix exprCheck(expr) - for e in [ + for e in ( 'ApF', 'Ap+F', 'Ap-F', @@ -236,12 +236,12 @@ def exprCheck(expr, output=None): "A'B'C.p1'2'3", ".D'E'Fp1'2'3", "A'B'C.D'E'Fp1'2'3", - ]: + ): expr = '0x' + e + suffix exprCheck(expr) exprCheck('"abc\\"cba"') # string # character literals - for p in ['', 'u8', 'u', 'U', 'L']: + for p in ('', 'u8', 'u', 'U', 'L'): exprCheck(p + "'a'") exprCheck(p + "'\\n'") exprCheck(p + "'\\012'") diff --git a/tests/test_domains/test_domain_cpp.py b/tests/test_domains/test_domain_cpp.py index 042ba47d915..6a7a7778d8c 100644 --- a/tests/test_domains/test_domain_cpp.py +++ b/tests/test_domains/test_domain_cpp.py @@ -50,7 +50,7 @@ def _check(name, input, idDict, output, key, asTextOutput): if key is None: key = name key += ' ' - if name in ('function', 'member'): + if name in {'function', 'member'}: inputActual = input outputAst = output outputAsText = output @@ -270,7 +270,7 @@ class Config: ".D'E'Fp1'2'3", "A'B'C.D'E'Fp1'2'3", ] - for suffix in ['', 'f', 'F', 'l', 'L']: + for suffix in ('', 'f', 'F', 'l', 'L'): for e in decimalFloats: expr = e + suffix exprCheck(expr, 'L' + expr.replace("'", '') + 'E') diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py index 74531d1097f..9f19f1da34c 100644 --- a/tests/test_domains/test_domain_py.py +++ b/tests/test_domains/test_domain_py.py @@ -572,34 +572,67 @@ def test_module_index(app): index = PythonModuleIndex(app.env.domains.python_domain) assert index.generate() == ( [ - ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), + ( + 'd', + [ + IndexEntry( + name='docutils', + subtype=0, + docname='index', + anchor='module-docutils', + extra='', + qualifier='', + descr='', + ), + ], + ), ( 's', [ - IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''), IndexEntry( - 'sphinx.builders', - 2, - 'index', - 'module-sphinx.builders', - '', - '', - '', + name='sphinx', + subtype=1, + docname='index', + anchor='module-sphinx', + extra='', + qualifier='', + descr='', + ), + IndexEntry( + name='sphinx.builders', + subtype=2, + docname='index', + anchor='module-sphinx.builders', + extra='', + qualifier='', + descr='', ), IndexEntry( - 'sphinx.builders.html', - 2, - 'index', - 'module-sphinx.builders.html', - '', - '', - '', + name='sphinx.builders.html', + subtype=2, + docname='index', + anchor='module-sphinx.builders.html', + extra='', + qualifier='', + descr='', ), IndexEntry( - 'sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '' + name='sphinx.config', + subtype=2, + docname='index', + anchor='module-sphinx.config', + extra='', + qualifier='', + descr='', ), IndexEntry( - 'sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '' + name='sphinx_intl', + subtype=0, + docname='index', + anchor='module-sphinx_intl', + extra='', + qualifier='', + descr='', ), ], ), @@ -618,9 +651,23 @@ def test_module_index_submodule(app): ( 's', [ - IndexEntry('sphinx', 1, '', '', '', '', ''), IndexEntry( - 'sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '' + name='sphinx', + subtype=1, + docname='', + anchor='', + extra='', + qualifier='', + descr='', + ), + IndexEntry( + name='sphinx.config', + subtype=2, + docname='index', + anchor='module-sphinx.config', + extra='', + qualifier='', + descr='', ), ], ) @@ -636,8 +683,34 @@ def test_module_index_not_collapsed(app): index = PythonModuleIndex(app.env.domains.python_domain) assert index.generate() == ( [ - ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), - ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', '')]), + ( + 'd', + [ + IndexEntry( + name='docutils', + subtype=0, + docname='index', + anchor='module-docutils', + extra='', + qualifier='', + descr='', + ), + ], + ), + ( + 's', + [ + IndexEntry( + name='sphinx', + subtype=0, + docname='index', + anchor='module-sphinx', + extra='', + qualifier='', + descr='', + ), + ], + ), ], True, ) @@ -666,22 +739,22 @@ def test_modindex_common_prefix(app): 'b', [ IndexEntry( - 'sphinx.builders', - 1, - 'index', - 'module-sphinx.builders', - '', - '', - '', + name='sphinx.builders', + subtype=1, + docname='index', + anchor='module-sphinx.builders', + extra='', + qualifier='', + descr='', ), IndexEntry( - 'sphinx.builders.html', - 2, - 'index', - 'module-sphinx.builders.html', - '', - '', - '', + name='sphinx.builders.html', + subtype=2, + docname='index', + anchor='module-sphinx.builders.html', + extra='', + qualifier='', + descr='', ), ], ), @@ -689,17 +762,50 @@ def test_modindex_common_prefix(app): 'c', [ IndexEntry( - 'sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '' - ) + name='sphinx.config', + subtype=0, + docname='index', + anchor='module-sphinx.config', + extra='', + qualifier='', + descr='', + ), + ], + ), + ( + 'd', + [ + IndexEntry( + name='docutils', + subtype=0, + docname='index', + anchor='module-docutils', + extra='', + qualifier='', + descr='', + ), ], ), - ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), ( 's', [ - IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''), IndexEntry( - 'sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '' + name='sphinx', + subtype=0, + docname='index', + anchor='module-sphinx', + extra='', + qualifier='', + descr='', + ), + IndexEntry( + name='sphinx_intl', + subtype=0, + docname='index', + anchor='module-sphinx_intl', + extra='', + qualifier='', + descr='', ), ], ), diff --git a/tests/test_environment/test_environment.py b/tests/test_environment/test_environment.py index b711fd879c4..64487b8c3f6 100644 --- a/tests/test_environment/test_environment.py +++ b/tests/test_environment/test_environment.py @@ -1,6 +1,5 @@ """Test the BuildEnvironment class.""" -import os import shutil from pathlib import Path @@ -41,8 +40,8 @@ def test_config_status(make_app, app_params): # incremental build (config entry changed) app3 = make_app(*args, confoverrides={'root_doc': 'indexx'}, **kwargs) - fname = os.path.join(app3.srcdir, 'index.rst') - assert os.path.isfile(fname) + fname = app3.srcdir / 'index.rst' + assert fname.is_file() shutil.move(fname, fname[:-4] + 'x.rst') assert app3.env.config_status == CONFIG_CHANGED app3.build() diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py index 36594133996..3886617c742 100644 --- a/tests/test_extensions/test_ext_apidoc.py +++ b/tests/test_extensions/test_ext_apidoc.py @@ -1,6 +1,5 @@ """Test the sphinx.apidoc module.""" -import os.path from collections import namedtuple from pathlib import Path @@ -365,7 +364,7 @@ def test_toc_all_references_should_exist_pep420_enabled(apidoc): found_refs = [] missing_files = [] for ref in refs: - if ref and ref[0] in (':', '#'): + if ref and ref[0] in {':', '#'}: continue found_refs.append(ref) filename = f'{ref}.rst' @@ -396,7 +395,7 @@ def test_toc_all_references_should_exist_pep420_disabled(apidoc): found_refs = [] missing_files = [] for ref in refs: - if ref and ref[0] in (':', '#'): + if ref and ref[0] in {':', '#'}: continue filename = f'{ref}.rst' found_refs.append(ref) @@ -732,7 +731,7 @@ def test_no_duplicates(rootdir, tmp_path): apidoc_main(['-o', str(outdir), '-T', str(package), '--implicit-namespaces']) # Ensure the module has been documented - assert os.path.isfile(outdir / 'fish_licence.rst') + assert (outdir / 'fish_licence.rst').is_file() # Ensure the submodule only appears once text = (outdir / 'fish_licence.rst').read_text(encoding='utf-8') diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index f483eae8a65..f2fb0c8ec11 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -133,7 +133,7 @@ def process_signature(app, what, name, obj, options, args, retann): return None def skip_member(app, what, name, obj, skip, options): - if name in ('__special1__', '__special2__'): + if name in {'__special1__', '__special2__'}: return skip if name.startswith('__'): return True @@ -1555,31 +1555,61 @@ def entry( qualname = f'{self.name}.{entry_name}' return self._node(role, qualname, doc, args=args, indent=indent, **rst_options) - def brief(self, doc: str, *, indent: int = 0, **options: Any) -> list[str]: - """Generate the brief part of the class being documented.""" + def preamble_lookup( + self, doc: str, *, indent: int = 0, **options: Any + ) -> list[str]: assert ( doc ), f'enumeration class {self.target!r} should have an explicit docstring' + args = self._preamble_args(functional_constructor=False) + return self._preamble(doc=doc, args=args, indent=indent, **options) + + def preamble_constructor( + self, doc: str, *, indent: int = 0, **options: Any + ) -> list[str]: + assert ( + doc + ), f'enumeration class {self.target!r} should have an explicit docstring' + + args = self._preamble_args(functional_constructor=True) + return self._preamble(doc=doc, args=args, indent=indent, **options) + + def _preamble( + self, *, doc: str, args: str, indent: int = 0, **options: Any + ) -> list[str]: + """Generate the preamble of the class being documented.""" + return self._node('class', self.name, doc, args=args, indent=indent, **options) + + @staticmethod + def _preamble_args(functional_constructor: bool = False): + """EnumType.__call__() is a dual-purpose method: + + * Look an enum member (valid only if the enum has members) + * Create a new enum class (functional API) + """ + if sys.version_info[:2] >= (3, 14): + if functional_constructor: + return ( + '(new_class_name, /, names, *, module=None, ' + 'qualname=None, type=None, start=1, boundary=None)' + ) + else: + return '(*values)' if sys.version_info[:2] >= (3, 13) or sys.version_info[:3] >= (3, 12, 3): - args = ( - '(value, names=, *values, module=None, ' - 'qualname=None, type=None, start=1, boundary=None)' - ) - elif sys.version_info[:2] >= (3, 12): - args = ( + if functional_constructor: + return ( + '(new_class_name, /, names, *, module=None, ' + 'qualname=None, type=None, start=1, boundary=None)' + ) + else: + return '(*values)' + if sys.version_info[:2] >= (3, 12): + return ( '(value, names=None, *values, module=None, ' 'qualname=None, type=None, start=1, boundary=None)' ) - elif sys.version_info[:2] >= (3, 11): - args = ( - '(value, names=None, *, module=None, qualname=None, ' - 'type=None, start=1, boundary=None)' - ) - else: - args = '(value)' - - return self._node('class', self.name, doc, args=args, indent=indent, **options) + return '(value)' def method( self, @@ -1612,7 +1642,7 @@ def test_enum_class(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method( 'say_goodbye', 'a classmethod says good-bye to you.', 'classmethod' ), @@ -1628,7 +1658,7 @@ def test_enum_class(app, autodoc_enum_options): # redefined by the user in one of the bases. actual = do_autodoc(app, 'class', fmt.target, options | {'inherited-members': None}) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method( 'say_goodbye', 'a classmethod says good-bye to you.', 'classmethod' ), @@ -1650,7 +1680,7 @@ def test_enum_class_with_data_type(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('say_goodbye', 'docstring', 'classmethod'), *fmt.method('say_hello', 'docstring'), *fmt.member('x', 'x', ''), @@ -1659,7 +1689,7 @@ def test_enum_class_with_data_type(app, autodoc_enum_options): options = autodoc_enum_options | {'inherited-members': None} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.entry('dtype', 'docstring', role='property'), *fmt.method('isupper', 'inherited'), *fmt.method('say_goodbye', 'docstring', 'classmethod'), @@ -1674,7 +1704,7 @@ def test_enum_class_with_mixin_type(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('say_goodbye', 'docstring', 'classmethod'), *fmt.method('say_hello', 'docstring'), *fmt.member('x', 'X', ''), @@ -1683,7 +1713,7 @@ def test_enum_class_with_mixin_type(app, autodoc_enum_options): options = autodoc_enum_options | {'inherited-members': None} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('say_goodbye', 'docstring', 'classmethod'), *fmt.method('say_hello', 'docstring'), *fmt.entry('value', 'uppercased', role='property'), @@ -1697,14 +1727,14 @@ def test_enum_class_with_mixin_type_and_inheritence(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.member('x', 'X', ''), ] options = autodoc_enum_options | {'inherited-members': None} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('say_goodbye', 'inherited', 'classmethod'), *fmt.method('say_hello', 'inherited'), *fmt.entry('value', 'uppercased', role='property'), @@ -1718,7 +1748,7 @@ def test_enum_class_with_mixin_enum_type(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), # override() is overridden at the class level so it should be rendered *fmt.method('override', 'overridden'), # say_goodbye() and say_hello() are not rendered since they are inherited @@ -1728,7 +1758,7 @@ def test_enum_class_with_mixin_enum_type(app, autodoc_enum_options): options = autodoc_enum_options | {'inherited-members': None} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('override', 'overridden'), *fmt.method('say_goodbye', 'inherited', 'classmethod'), *fmt.method('say_hello', 'inherited'), @@ -1742,7 +1772,7 @@ def test_enum_class_with_mixin_and_data_type(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('isupper', 'overridden'), *fmt.method('say_goodbye', 'overridden', 'classmethod'), *fmt.method('say_hello', 'overridden'), @@ -1753,7 +1783,7 @@ def test_enum_class_with_mixin_and_data_type(app, autodoc_enum_options): options = autodoc_enum_options | {'special-members': '__str__'} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('__str__', 'overridden'), *fmt.method('isupper', 'overridden'), *fmt.method('say_goodbye', 'overridden', 'classmethod'), @@ -1764,7 +1794,7 @@ def test_enum_class_with_mixin_and_data_type(app, autodoc_enum_options): options = autodoc_enum_options | {'inherited-members': None} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.entry('dtype', 'docstring', role='property'), *fmt.method('isupper', 'overridden'), *fmt.method('say_goodbye', 'overridden', 'classmethod'), @@ -1780,7 +1810,7 @@ def test_enum_with_parent_enum(app, autodoc_enum_options): actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('isupper', 'overridden'), *fmt.member('x', 'X', ''), ] @@ -1789,7 +1819,7 @@ def test_enum_with_parent_enum(app, autodoc_enum_options): options = autodoc_enum_options | {'special-members': '__str__'} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.method('__str__', 'overridden'), *fmt.method('isupper', 'overridden'), *fmt.member('x', 'X', ''), @@ -1798,7 +1828,7 @@ def test_enum_with_parent_enum(app, autodoc_enum_options): options = autodoc_enum_options | {'inherited-members': None} actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_lookup('this is enum class'), *fmt.entry('dtype', 'docstring', role='property'), *fmt.method('isupper', 'overridden'), *fmt.method('override', 'inherited'), @@ -1815,28 +1845,28 @@ def test_enum_sunder_method(app, autodoc_enum_options): fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] fmt = _EnumFormatter('EnumSunderMissingInEnumMixin') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] fmt = _EnumFormatter('EnumSunderMissingInDataType') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] fmt = _EnumFormatter('EnumSunderMissingInClass') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'), ] @@ -1851,21 +1881,21 @@ def test_enum_inherited_sunder_method(app, autodoc_enum_options): fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), ] fmt = _EnumFormatter('EnumSunderMissingInEnumMixin') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), ] fmt = _EnumFormatter('EnumSunderMissingInDataType') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), *fmt.entry('dtype', 'docstring', role='property'), *fmt.method('isupper', 'inherited'), @@ -1874,7 +1904,7 @@ def test_enum_inherited_sunder_method(app, autodoc_enum_options): fmt = _EnumFormatter('EnumSunderMissingInClass') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'), ] @@ -1883,20 +1913,20 @@ def test_enum_inherited_sunder_method(app, autodoc_enum_options): def test_enum_custom_name_property(app, autodoc_enum_options): fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] fmt = _EnumFormatter('EnumNamePropertyInEnumMixin') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] fmt = _EnumFormatter('EnumNamePropertyInDataType') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) - assert list(actual) == [*fmt.brief('this is enum class')] + assert list(actual) == [*fmt.preamble_constructor('this is enum class')] fmt = _EnumFormatter('EnumNamePropertyInClass') actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.entry('name', 'docstring', role='property'), ] @@ -1908,21 +1938,21 @@ def test_enum_inherited_custom_name_property(app, autodoc_enum_options): fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.entry('name', 'inherited', role='property'), ] fmt = _EnumFormatter('EnumNamePropertyInEnumMixin') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.entry('name', 'inherited', role='property'), ] fmt = _EnumFormatter('EnumNamePropertyInDataType') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.entry('dtype', 'docstring', role='property'), *fmt.method('isupper', 'inherited'), *fmt.entry('name', 'inherited', role='property'), @@ -1931,7 +1961,7 @@ def test_enum_inherited_custom_name_property(app, autodoc_enum_options): fmt = _EnumFormatter('EnumNamePropertyInClass') actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - *fmt.brief('this is enum class'), + *fmt.preamble_constructor('this is enum class'), *fmt.entry('name', 'docstring', role='property'), ] diff --git a/tests/test_extensions/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py index eaa133214eb..53200f54424 100644 --- a/tests/test_extensions/test_ext_autodoc_autoclass.py +++ b/tests/test_extensions/test_ext_autodoc_autoclass.py @@ -393,7 +393,7 @@ def autodoc_process_docstring(*args): """A handler always raises an error. This confirms this handler is never called for class aliases. """ - raise + raise RuntimeError app.connect('autodoc-process-docstring', autodoc_process_docstring) actual = do_autodoc(app, 'class', 'target.classes.Alias') diff --git a/tests/test_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py index ea4d2f3d944..c3911b8533c 100644 --- a/tests/test_extensions/test_ext_autodoc_configs.py +++ b/tests/test_extensions/test_ext_autodoc_configs.py @@ -2,7 +2,9 @@ import platform import sys +from collections.abc import Iterator from contextlib import contextmanager +from pathlib import Path import pytest @@ -10,11 +12,16 @@ from tests.test_extensions.autodoc_util import do_autodoc +skip_py314_segfault = pytest.mark.skipif( + sys.version_info[:2] >= (3, 14), + reason='Segmentation fault: https://github.com/python/cpython/issues/125017', +) + IS_PYPY = platform.python_implementation() == 'PyPy' @contextmanager -def overwrite_file(path, content): +def overwrite_file(path: Path, content: str) -> Iterator[None]: current_content = path.read_bytes() if path.exists() else None try: path.write_text(content, encoding='utf-8') @@ -182,6 +189,7 @@ def test_autodoc_class_signature_separated_init(app): ] +@skip_py314_segfault @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_class_signature_separated_new(app): app.config.autodoc_class_signature = 'separated' @@ -365,6 +373,7 @@ def test_autodoc_inherit_docstrings_for_inherited_members(app): ] +@skip_py314_segfault @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_docstring_signature(app): options = {'members': None, 'special-members': '__init__, __new__'} @@ -692,10 +701,6 @@ def test_mocked_module_imports(app): confoverrides={'autodoc_typehints': 'signature'}, ) def test_autodoc_typehints_signature(app): - if sys.version_info[:2] <= (3, 10): - type_o = '~typing.Any | None' - else: - type_o = '~typing.Any' if sys.version_info[:2] >= (3, 13): type_ppp = 'pathlib._local.PurePosixPath' else: @@ -732,7 +737,7 @@ def test_autodoc_typehints_signature(app): ' docstring', '', '', - '.. py:class:: Math(s: str, o: %s = None)' % type_o, + '.. py:class:: Math(s: str, o: ~typing.Any = None)', ' :module: target.typehints', '', '', @@ -1511,10 +1516,6 @@ def test_autodoc_typehints_description_and_type_aliases(app): confoverrides={'autodoc_typehints_format': 'fully-qualified'}, ) def test_autodoc_typehints_format_fully_qualified(app): - if sys.version_info[:2] <= (3, 10): - type_o = 'typing.Any | None' - else: - type_o = 'typing.Any' if sys.version_info[:2] >= (3, 13): type_ppp = 'pathlib._local.PurePosixPath' else: @@ -1550,7 +1551,7 @@ def test_autodoc_typehints_format_fully_qualified(app): ' docstring', '', '', - '.. py:class:: Math(s: str, o: %s = None)' % type_o, + '.. py:class:: Math(s: str, o: typing.Any = None)', ' :module: target.typehints', '', '', diff --git a/tests/test_extensions/test_ext_autodoc_events.py b/tests/test_extensions/test_ext_autodoc_events.py index 733b0be337b..8ff8630ad7e 100644 --- a/tests/test_extensions/test_ext_autodoc_events.py +++ b/tests/test_extensions/test_ext_autodoc_events.py @@ -29,7 +29,7 @@ def on_process_docstring(app, what, name, obj, options, lines): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_process_docstring_for_nondatadescriptor(app): def on_process_docstring(app, what, name, obj, options, lines): - raise + raise RuntimeError app.connect('autodoc-process-docstring', on_process_docstring) @@ -58,6 +58,26 @@ def test_cut_lines(app): ] +def test_cut_lines_no_objtype(): + docstring_lines = [ + 'first line', + '---', + 'second line', + '---', + 'third line ', + '', + ] + process = cut_lines(2) + + process(None, 'function', 'func', None, {}, docstring_lines) # type: ignore[arg-type] + assert docstring_lines == [ + 'second line', + '---', + 'third line ', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_between(app): app.connect('autodoc-process-docstring', between('---', ['function'])) diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index a6bf97a2227..81b13860278 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -1,6 +1,7 @@ """Test the autosummary extension.""" import sys +from contextlib import chdir from io import StringIO from unittest.mock import Mock, patch from xml.etree.ElementTree import Element @@ -25,11 +26,6 @@ from sphinx.testing.util import assert_node, etree_parse from sphinx.util.docutils import new_document -try: - from contextlib import chdir -except ImportError: - from sphinx.util.osutil import _chdir as chdir - html_warnfile = StringIO() @@ -341,7 +337,7 @@ def test_autosummary_generate_content_for_module_skipped(app): template = Mock() def skip_member(app, what, name, obj, skip, options): - if name in ('Foo', 'bar', 'Exc'): + if name in {'Foo', 'bar', 'Exc'}: return True return None diff --git a/tests/test_extensions/test_ext_doctest.py b/tests/test_extensions/test_ext_doctest.py index 198d2ddc415..0f141f276d2 100644 --- a/tests/test_extensions/test_ext_doctest.py +++ b/tests/test_extensions/test_ext_doctest.py @@ -15,7 +15,7 @@ @pytest.mark.sphinx('doctest', testroot='ext-doctest') def test_build(app): - global cleanup_called + global cleanup_called # NoQA: PLW0603 cleanup_called = 0 app.build(force_all=True) assert app.statuscode == 0, f'failures in doctests:\n{app.status.getvalue()}' @@ -69,7 +69,7 @@ def test_is_allowed_version(): def cleanup_call(): - global cleanup_called + global cleanup_called # NoQA: PLW0603 cleanup_called += 1 @@ -87,7 +87,7 @@ def test_skipif(app): in ``test_build`` above, and the assertion below would fail. """ - global recorded_calls + global recorded_calls # NoQA: PLW0603 recorded_calls = Counter() app.build(force_all=True) if app.statuscode != 0: diff --git a/tests/test_extensions/test_ext_inheritance_diagram.py b/tests/test_extensions/test_ext_inheritance_diagram.py index 2a2166f4cad..2aa4ff99188 100644 --- a/tests/test_extensions/test_ext_inheritance_diagram.py +++ b/tests/test_extensions/test_ext_inheritance_diagram.py @@ -1,9 +1,9 @@ """Test sphinx.ext.inheritance_diagram extension.""" -import os import re import sys import zlib +from pathlib import Path import pytest @@ -26,7 +26,7 @@ def test_inheritance_diagram(app): def new_run(self): result = orig_run(self) node = result[0] - source = os.path.basename(node.document.current_source).replace('.rst', '') + source = Path(node.document.current_source).stem graphs[source] = node['graph'] return result @@ -48,25 +48,25 @@ def new_run(self): # basic inheritance diagram showing all classes for cls in graphs['basic_diagram'].class_info: # use in b/c traversing order is different sometimes - assert cls in [ - ('dummy.test.A', 'dummy.test.A', [], None), - ('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None), - ('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None), - ('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None), - ('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None), - ('dummy.test.B', 'dummy.test.B', ['dummy.test.A'], None), - ] + assert cls in { + ('dummy.test.A', 'dummy.test.A', (), None), + ('dummy.test.F', 'dummy.test.F', ('dummy.test.C',), None), + ('dummy.test.C', 'dummy.test.C', ('dummy.test.A',), None), + ('dummy.test.E', 'dummy.test.E', ('dummy.test.B',), None), + ('dummy.test.D', 'dummy.test.D', ('dummy.test.B', 'dummy.test.C'), None), + ('dummy.test.B', 'dummy.test.B', ('dummy.test.A',), None), + } # inheritance diagram using :parts: 1 option for cls in graphs['diagram_w_parts'].class_info: - assert cls in [ - ('A', 'dummy.test.A', [], None), - ('F', 'dummy.test.F', ['C'], None), - ('C', 'dummy.test.C', ['A'], None), - ('E', 'dummy.test.E', ['B'], None), - ('D', 'dummy.test.D', ['B', 'C'], None), - ('B', 'dummy.test.B', ['A'], None), - ] + assert cls in { + ('A', 'dummy.test.A', (), None), + ('F', 'dummy.test.F', ('C',), None), + ('C', 'dummy.test.C', ('A',), None), + ('E', 'dummy.test.E', ('B',), None), + ('D', 'dummy.test.D', ('B', 'C'), None), + ('B', 'dummy.test.B', ('A',), None), + } # inheritance diagram with 1 top class # :top-classes: dummy.test.B @@ -78,14 +78,14 @@ def new_run(self): # E D F # for cls in graphs['diagram_w_1_top_class'].class_info: - assert cls in [ - ('dummy.test.A', 'dummy.test.A', [], None), - ('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None), - ('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None), - ('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None), - ('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None), - ('dummy.test.B', 'dummy.test.B', [], None), - ] + assert cls in { + ('dummy.test.A', 'dummy.test.A', (), None), + ('dummy.test.F', 'dummy.test.F', ('dummy.test.C',), None), + ('dummy.test.C', 'dummy.test.C', ('dummy.test.A',), None), + ('dummy.test.E', 'dummy.test.E', ('dummy.test.B',), None), + ('dummy.test.D', 'dummy.test.D', ('dummy.test.B', 'dummy.test.C'), None), + ('dummy.test.B', 'dummy.test.B', (), None), + } # inheritance diagram with 2 top classes # :top-classes: dummy.test.B, dummy.test.C @@ -97,13 +97,13 @@ def new_run(self): # E D F # for cls in graphs['diagram_w_2_top_classes'].class_info: - assert cls in [ - ('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None), - ('dummy.test.C', 'dummy.test.C', [], None), - ('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None), - ('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None), - ('dummy.test.B', 'dummy.test.B', [], None), - ] + assert cls in { + ('dummy.test.F', 'dummy.test.F', ('dummy.test.C',), None), + ('dummy.test.C', 'dummy.test.C', (), None), + ('dummy.test.E', 'dummy.test.E', ('dummy.test.B',), None), + ('dummy.test.D', 'dummy.test.D', ('dummy.test.B', 'dummy.test.C'), None), + ('dummy.test.B', 'dummy.test.B', (), None), + } # inheritance diagram with 2 top classes and specifying the entire module # rendering should be @@ -119,27 +119,27 @@ def new_run(self): # If you'd like to not show class A in the graph don't specify the entire module. # this is a known issue. for cls in graphs['diagram_module_w_2_top_classes'].class_info: - assert cls in [ - ('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None), - ('dummy.test.C', 'dummy.test.C', [], None), - ('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None), - ('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None), - ('dummy.test.B', 'dummy.test.B', [], None), - ('dummy.test.A', 'dummy.test.A', [], None), - ] + assert cls in { + ('dummy.test.F', 'dummy.test.F', ('dummy.test.C',), None), + ('dummy.test.C', 'dummy.test.C', (), None), + ('dummy.test.E', 'dummy.test.E', ('dummy.test.B',), None), + ('dummy.test.D', 'dummy.test.D', ('dummy.test.B', 'dummy.test.C'), None), + ('dummy.test.B', 'dummy.test.B', (), None), + ('dummy.test.A', 'dummy.test.A', (), None), + } # inheritance diagram involving a base class nested within another class for cls in graphs['diagram_w_nested_classes'].class_info: - assert cls in [ - ('dummy.test_nested.A', 'dummy.test_nested.A', [], None), + assert cls in { + ('dummy.test_nested.A', 'dummy.test_nested.A', (), None), ( 'dummy.test_nested.C', 'dummy.test_nested.C', - ['dummy.test_nested.A.B'], + ('dummy.test_nested.A.B',), None, ), - ('dummy.test_nested.A.B', 'dummy.test_nested.A.B', [], None), - ] + ('dummy.test_nested.A.B', 'dummy.test_nested.A.B', (), None), + } # An external inventory to test intersphinx links in inheritance diagrams @@ -293,17 +293,17 @@ def test_inheritance_diagram_latex_alias(app): assert ( 'test.DocSubDir2', 'test.DocSubDir2', - ['test.DocSubDir1'], + ('test.DocSubDir1',), None, ) in aliased_graph assert ( 'test.DocSubDir1', 'test.DocSubDir1', - ['test.DocHere'], + ('test.DocHere',), None, ) in aliased_graph - assert ('test.DocHere', 'test.DocHere', ['alias.Foo'], None) in aliased_graph - assert ('alias.Foo', 'alias.Foo', [], None) in aliased_graph + assert ('test.DocHere', 'test.DocHere', ('alias.Foo',), None) in aliased_graph + assert ('alias.Foo', 'alias.Foo', (), None) in aliased_graph content = (app.outdir / 'index.html').read_text(encoding='utf8') diff --git a/tests/test_extensions/test_ext_napoleon.py b/tests/test_extensions/test_ext_napoleon.py index 59477dfe064..a14cb55dda2 100644 --- a/tests/test_extensions/test_ext_napoleon.py +++ b/tests/test_extensions/test_ext_napoleon.py @@ -48,15 +48,15 @@ def _private_doc(self): def _private_undoc(self): pass - def __special_doc__(self): + def __special_doc__(self): # NoQA: PLW3201 """SampleClass.__special_doc__.DOCSTRING""" pass - def __special_undoc__(self): + def __special_undoc__(self): # NoQA: PLW3201 pass @simple_decorator - def __decorated_func__(self): + def __decorated_func__(self): # NoQA: PLW3201 """Doc""" pass @@ -69,11 +69,11 @@ def _private_doc(self): def _private_undoc(self): pass - def __special_doc__(self): + def __special_doc__(self): # NoQA: PLW3201 """SampleError.__special_doc__.DOCSTRING""" pass - def __special_undoc__(self): + def __special_undoc__(self): # NoQA: PLW3201 pass diff --git a/tests/test_extensions/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py index c4250ef594a..60a2eaa2239 100644 --- a/tests/test_extensions/test_ext_napoleon_docstring.py +++ b/tests/test_extensions/test_ext_napoleon_docstring.py @@ -1579,12 +1579,13 @@ def test_sphinx_admonitions(self): config = Config() for section, admonition in admonition_map.items(): # Multiline + underline = '-' * len(section) actual = NumpyDocstring( - f"{section}\n" - f"{'-' * len(section)}\n" - " this is the first line\n" - "\n" - " and this is the second line\n", + f'{section}\n' + f'{underline}\n' + ' this is the first line\n' + '\n' + ' and this is the second line\n', config, ) expect = ( diff --git a/tests/test_intl/test_intl.py b/tests/test_intl/test_intl.py index 6f343e03dcb..077516b0a05 100644 --- a/tests/test_intl/test_intl.py +++ b/tests/test_intl/test_intl.py @@ -4,7 +4,6 @@ """ import os -import os.path import re import shutil import time @@ -66,7 +65,7 @@ def _info(app): def elem_gettexts(elem): - return [_f for _f in [s.strip() for s in elem.itertext()] if _f] + return list(filter(None, map(str.strip, elem.itertext()))) def elem_getref(elem): @@ -368,8 +367,9 @@ def test_gettext_section(app): # --- section expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po') actual = read_po(app.outdir / 'section.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl @@ -380,7 +380,7 @@ def test_text_section(app): # --- section result = (app.outdir / 'section.txt').read_text(encoding='utf8') expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po') - for expect_msg in [m for m in expect if m.id]: + for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.string in result @@ -537,13 +537,15 @@ def test_gettext_toctree(app): # --- toctree (index.rst) expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'index.po') actual = read_po(app.outdir / 'index.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids # --- toctree (toctree.rst) expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po') actual = read_po(app.outdir / 'toctree.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl @@ -554,8 +556,9 @@ def test_gettext_table(app): # --- toctree expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po') actual = read_po(app.outdir / 'table.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl @@ -566,7 +569,7 @@ def test_text_table(app): # --- toctree result = (app.outdir / 'table.txt').read_text(encoding='utf8') expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po') - for expect_msg in [m for m in expect if m.id]: + for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.string in result @@ -595,8 +598,9 @@ def test_gettext_topic(app): # --- topic expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po') actual = read_po(app.outdir / 'topic.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl @@ -607,7 +611,7 @@ def test_text_topic(app): # --- topic result = (app.outdir / 'topic.txt').read_text(encoding='utf8') expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po') - for expect_msg in [m for m in expect if m.id]: + for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.string in result @@ -621,8 +625,9 @@ def test_gettext_definition_terms(app): app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'definition_terms.po' ) actual = read_po(app.outdir / 'definition_terms.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl @@ -633,8 +638,9 @@ def test_gettext_glossary_terms(app): # --- glossary terms: regression test for #1090 expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms.po') actual = read_po(app.outdir / 'glossary_terms.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids warnings = app.warning.getvalue().replace(os.sep, '/') assert 'term not in glossary' not in warnings @@ -649,8 +655,9 @@ def test_gettext_glossary_term_inconsistencies(app): app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po' ) actual = read_po(app.outdir / 'glossary_terms_inconsistency.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl @@ -661,10 +668,11 @@ def test_gettext_literalblock(app): # --- gettext builder always ignores ``only`` directive expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'literalblock.po') actual = read_po(app.outdir / 'literalblock.pot') - for expect_msg in [m for m in expect if m.id]: + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): if len(expect_msg.id.splitlines()) == 1: # compare translations only labels - assert expect_msg.id in [m.id for m in actual if m.id] + assert expect_msg.id in actual_msg_ids else: pass # skip code-blocks and literalblocks @@ -677,8 +685,9 @@ def test_gettext_buildr_ignores_only_directive(app): # --- gettext builder always ignores ``only`` directive expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'only.po') actual = read_po(app.outdir / 'only.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] + actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] + for expect_msg in (msg for msg in expect if msg.id): + assert expect_msg.id in actual_msg_ids @sphinx_intl diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py index 98052de2572..1a8a074151c 100644 --- a/tests/test_markup/test_markup.py +++ b/tests/test_markup/test_markup.py @@ -166,14 +166,14 @@ def get(name): ':cve:`2020-10735`', ( '

' + 'href="https://www.cve.org/CVERecord?id=CVE-2020-10735">' 'CVE 2020-10735

' ), ( '\\sphinxAtStartPar\n' '\\index{Common Vulnerabilities and Exposures@\\spxentry{Common Vulnerabilities and Exposures}' '!CVE 2020\\sphinxhyphen{}10735@\\spxentry{CVE 2020\\sphinxhyphen{}10735}}' - '\\sphinxhref{https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10735}' + '\\sphinxhref{https://www.cve.org/CVERecord?id=CVE-2020-10735}' '{\\sphinxstylestrong{CVE 2020\\sphinxhyphen{}10735}}' ), ), @@ -183,14 +183,14 @@ def get(name): ':cve:`2020-10735#id1`', ( '

' + 'href="https://www.cve.org/CVERecord?id=CVE-2020-10735#id1">' 'CVE 2020-10735#id1

' ), ( '\\sphinxAtStartPar\n' '\\index{Common Vulnerabilities and Exposures@\\spxentry{Common Vulnerabilities and Exposures}' '!CVE 2020\\sphinxhyphen{}10735\\#id1@\\spxentry{CVE 2020\\sphinxhyphen{}10735\\#id1}}' - '\\sphinxhref{https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10735\\#id1}' + '\\sphinxhref{https://www.cve.org/CVERecord?id=CVE-2020-10735\\#id1}' '{\\sphinxstylestrong{CVE 2020\\sphinxhyphen{}10735\\#id1}}' ), ), diff --git a/tests/test_pycode/test_pycode.py b/tests/test_pycode/test_pycode.py index 6bc71bdcb87..3a34d6f3a0f 100644 --- a/tests/test_pycode/test_pycode.py +++ b/tests/test_pycode/test_pycode.py @@ -1,7 +1,7 @@ """Test pycode.""" -import os import sys +from pathlib import Path import pytest @@ -9,7 +9,7 @@ from sphinx.errors import PycodeError from sphinx.pycode import ModuleAnalyzer -SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py' +SPHINX_MODULE_PATH = Path(sphinx.__file__).resolve().with_suffix('.py') def test_ModuleAnalyzer_get_module_source(): @@ -40,7 +40,7 @@ def test_ModuleAnalyzer_for_file(): def test_ModuleAnalyzer_for_module(rootdir): analyzer = ModuleAnalyzer.for_module('sphinx') assert analyzer.modname == 'sphinx' - assert analyzer.srcname in (SPHINX_MODULE_PATH, os.path.abspath(SPHINX_MODULE_PATH)) + assert analyzer.srcname == str(SPHINX_MODULE_PATH) saved_path = sys.path.copy() sys.path.insert(0, str(rootdir / 'test-pycode')) diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 0507d0c8243..07a61689575 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -3,7 +3,6 @@ import time from collections.abc import Callable from io import StringIO -from os import path from pathlib import Path from typing import Any @@ -31,9 +30,9 @@ def input_(prompt: str) -> str: 'answer for %r missing and no default present' % prompt ) called.add(prompt) - for question in answers: + for question, answer in answers.items(): if prompt.startswith(qs.PROMPT_PREFIX + question): - return answers[question] + return answer if needanswer: raise AssertionError('answer for %r missing' % prompt) return '' @@ -260,10 +259,7 @@ def test_extensions(tmp_path): def test_exits_when_existing_confpy(monkeypatch): # The code detects existing conf.py with path.is_file() # so we mock it as True with pytest's monkeypatch - def mock_isfile(path): - return True - - monkeypatch.setattr(path, 'isfile', mock_isfile) + monkeypatch.setattr('os.path.isfile', lambda path: True) qs.term_input = mock_input({ 'Please enter a new root path (or just Enter to exit)': '', diff --git a/tests/test_search.py b/tests/test_search.py index 31918587a69..f74755b3b5b 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -62,20 +62,6 @@ def get_objects(self) -> list[tuple[str, str, str, str, str, int]]: return self.data -settings = parser = None - - -def setup_module(): - global settings, parser - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', category=DeprecationWarning) - # DeprecationWarning: The frontend.OptionParser class will be replaced - # by a subclass of argparse.ArgumentParser in Docutils 0.21 or later. - optparser = frontend.OptionParser(components=(rst.Parser,)) - settings = optparser.get_default_values() - parser = rst.Parser() - - def load_searchindex(path: Path) -> Any: searchindex = path.read_text(encoding='utf8') assert searchindex.startswith('Search.setIndex(') @@ -166,8 +152,8 @@ def test_term_in_heading_and_section(app): # if search term is in the title of one doc and in the text of another # both documents should be a hit in the search index as a title, # respectively text hit - assert '"textinhead": 2' in searchindex - assert '"textinhead": 0' in searchindex + assert '"textinhead":2' in searchindex + assert '"textinhead":0' in searchindex @pytest.mark.sphinx('html', testroot='search') @@ -180,6 +166,14 @@ def test_term_in_raw_directive(app): def test_IndexBuilder(): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=DeprecationWarning) + # DeprecationWarning: The frontend.OptionParser class will be replaced + # by a subclass of argparse.ArgumentParser in Docutils 0.21 or later. + optparser = frontend.OptionParser(components=(rst.Parser,)) + settings = optparser.get_default_values() + parser = rst.Parser() + domain1 = DummyDomain( 'dummy1', [ diff --git a/tests/test_theming/test_theming.py b/tests/test_theming/test_theming.py index f0c159a1237..afd15f838b4 100644 --- a/tests/test_theming/test_theming.py +++ b/tests/test_theming/test_theming.py @@ -1,6 +1,5 @@ """Test the Theme class.""" -import os import shutil from pathlib import Path from xml.etree.ElementTree import ParseError @@ -52,11 +51,11 @@ def test_theme_api(app): # test Theme class API assert set(app.registry.html_themes.keys()) == set(themes) - assert app.registry.html_themes['test-theme'] == str( + assert app.registry.html_themes['test-theme'] == ( app.srcdir / 'test_theme' / 'test-theme' ) - assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip') - assert app.registry.html_themes['staticfiles'] == str( + assert app.registry.html_themes['ziptheme'] == (app.srcdir / 'ziptheme.zip') + assert app.registry.html_themes['staticfiles'] == ( app.srcdir / 'test_theme' / 'staticfiles' ) @@ -88,14 +87,14 @@ def test_theme_api(app): # cleanup temp directories theme._cleanup() - assert not any(map(os.path.exists, theme._tmp_dirs)) + assert not any(p.exists() for p in theme._tmp_dirs) def test_nonexistent_theme_settings(tmp_path): # Check that error occurs with a non-existent theme.toml or theme.conf # (https://github.com/sphinx-doc/sphinx/issues/11668) with pytest.raises(ThemeError): - _load_theme('', str(tmp_path)) + _load_theme('', tmp_path) @pytest.mark.sphinx('html', testroot='double-inheriting-theme') @@ -224,7 +223,7 @@ def test_theme_builds(make_app, rootdir, sphinx_test_tempdir, theme_name): def test_config_file_toml(): config_path = HERE / 'theme.toml' - cfg = _load_theme_toml(str(config_path)) + cfg = _load_theme_toml(config_path) config = _convert_theme_toml(cfg) assert config == _ConfigFile( @@ -238,7 +237,7 @@ def test_config_file_toml(): def test_config_file_conf(): config_path = HERE / 'theme.conf' - cfg = _load_theme_conf(str(config_path)) + cfg = _load_theme_conf(config_path) config = _convert_theme_conf(cfg) assert config == _ConfigFile( diff --git a/tests/test_util/test_util.py b/tests/test_util/test_util.py index b36ad0ee93c..b04c30e7aa8 100644 --- a/tests/test_util/test_util.py +++ b/tests/test_util/test_util.py @@ -1,16 +1,12 @@ """Tests util functions.""" -import os -import tempfile - from sphinx.util.osutil import ensuredir -def test_ensuredir(): - with tempfile.TemporaryDirectory() as tmp_path: - # Does not raise an exception for an existing directory. - ensuredir(tmp_path) +def test_ensuredir(tmp_path): + # Does not raise an exception for an existing directory. + ensuredir(tmp_path) - path = os.path.join(tmp_path, 'a', 'b', 'c') - ensuredir(path) - assert os.path.isdir(path) + path = tmp_path / 'a' / 'b' / 'c' + ensuredir(path) + assert path.is_dir() diff --git a/tests/test_util/test_util_console.py b/tests/test_util/test_util_console.py index 3343091aede..fb48e189f30 100644 --- a/tests/test_util/test_util_console.py +++ b/tests/test_util/test_util_console.py @@ -78,7 +78,7 @@ def test_strip_ansi_short_forms(): # some messages use '\x1b[0m' instead of ``reset(s)``, so we # test whether this alternative form is supported or not. - for strip_function in [strip_colors, strip_escape_sequences]: + for strip_function in strip_colors, strip_escape_sequences: # \x1b[m and \x1b[0m are equivalent to \x1b[00m assert strip_function('\x1b[m') == '' assert strip_function('\x1b[0m') == '' diff --git a/tests/test_util/test_util_display.py b/tests/test_util/test_util_display.py index 4a3c9266b9a..afad5a34d90 100644 --- a/tests/test_util/test_util_display.py +++ b/tests/test_util/test_util_display.py @@ -91,7 +91,7 @@ def test_progress_message(app): # error case try: with progress_message('testing'): - raise + raise RuntimeError except Exception: pass diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 5bfa4adab63..973b054a1d8 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -3,6 +3,7 @@ import datetime import os import time +from pathlib import Path import babel import pytest @@ -18,16 +19,16 @@ def test_catalog_info_for_file_and_path(): cat = i18n.CatalogInfo('path', 'domain', 'utf-8') assert cat.po_file == 'domain.po' assert cat.mo_file == 'domain.mo' - assert cat.po_path == os.path.join('path', 'domain.po') - assert cat.mo_path == os.path.join('path', 'domain.mo') + assert cat.po_path == str(Path('path', 'domain.po')) + assert cat.mo_path == str(Path('path', 'domain.mo')) def test_catalog_info_for_sub_domain_file_and_path(): cat = i18n.CatalogInfo('path', 'sub/domain', 'utf-8') assert cat.po_file == 'sub/domain.po' assert cat.mo_file == 'sub/domain.mo' - assert cat.po_path == os.path.join('path', 'sub/domain.po') - assert cat.mo_path == os.path.join('path', 'sub/domain.mo') + assert cat.po_path == str(Path('path', 'sub', 'domain.po')) + assert cat.mo_path == str(Path('path', 'sub', 'domain.mo')) def test_catalog_outdated(tmp_path): @@ -48,8 +49,9 @@ def test_catalog_write_mo(tmp_path): (tmp_path / 'test.po').write_text('#', encoding='utf8') cat = i18n.CatalogInfo(tmp_path, 'test', 'utf-8') cat.write_mo('en') - assert os.path.exists(cat.mo_path) - with open(cat.mo_path, 'rb') as f: + mo_path = Path(cat.mo_path) + assert mo_path.exists() + with open(mo_path, 'rb') as f: assert read_mo(f) is not None @@ -95,7 +97,7 @@ def test_format_date(): def test_format_date_timezone(): - dt = datetime.datetime(2016, 8, 7, 5, 11, 17, 0, tzinfo=datetime.timezone.utc) + dt = datetime.datetime(2016, 8, 7, 5, 11, 17, 0, tzinfo=datetime.UTC) if time.localtime(dt.timestamp()).tm_gmtoff == 0: raise pytest.skip('Local time zone is GMT') # NoQA: EM101 diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 33226371fb7..5c868efa6e9 100644 --- a/tests/test_util/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -6,7 +6,6 @@ import datetime import enum import functools -import sys import types from inspect import Parameter from typing import Callable, List, Optional, Union # NoQA: UP035 @@ -95,11 +94,11 @@ def wrapper(): def test_TypeAliasForwardRef(): alias = TypeAliasForwardRef('example') sig_str = stringify_annotation(alias, 'fully-qualified-except-typing') - assert sig_str == 'example' + assert sig_str == "TypeAliasForwardRef('example')" alias = Optional[alias] # NoQA: UP007 sig_str = stringify_annotation(alias, 'fully-qualified-except-typing') - assert sig_str == 'example | None' + assert sig_str == "TypeAliasForwardRef('example') | None" def test_TypeAliasNamespace(): @@ -272,10 +271,7 @@ def test_signature_annotations(): # Space around '=' for defaults sig = inspect.signature(mod.f7) sig_str = stringify_signature(sig) - if sys.version_info[:2] <= (3, 10): - assert sig_str == '(x: int | None = None, y: dict = {}) -> None' - else: - assert sig_str == '(x: int = None, y: dict = {}) -> None' + assert sig_str == '(x: int = None, y: dict = {}) -> None' # Callable types sig = inspect.signature(mod.f8) @@ -303,10 +299,10 @@ def test_signature_annotations(): # optional union sig = inspect.signature(mod.f20) - assert stringify_signature(sig) in ( + assert stringify_signature(sig) in { '() -> int | str | None', '() -> str | int | None', - ) + } # Any sig = inspect.signature(mod.f14) @@ -355,18 +351,12 @@ def test_signature_annotations(): # show_return_annotation is False sig = inspect.signature(mod.f7) sig_str = stringify_signature(sig, show_return_annotation=False) - if sys.version_info[:2] <= (3, 10): - assert sig_str == '(x: int | None = None, y: dict = {})' - else: - assert sig_str == '(x: int = None, y: dict = {})' + assert sig_str == '(x: int = None, y: dict = {})' # unqualified_typehints is True sig = inspect.signature(mod.f7) sig_str = stringify_signature(sig, unqualified_typehints=True) - if sys.version_info[:2] <= (3, 10): - assert sig_str == '(x: int | None = None, y: dict = {}) -> None' - else: - assert sig_str == '(x: int = None, y: dict = {}) -> None' + assert sig_str == '(x: int = None, y: dict = {}) -> None' # case: separator at head sig = inspect.signature(mod.f22) diff --git a/tests/test_util/test_util_logging.py b/tests/test_util/test_util_logging.py index 9d3e5023aea..5a9e0be5ada 100644 --- a/tests/test_util/test_util_logging.py +++ b/tests/test_util/test_util_logging.py @@ -2,12 +2,12 @@ import codecs import os -import os.path +from pathlib import Path import pytest from docutils import nodes -from sphinx.util import logging, osutil +from sphinx.util import logging from sphinx.util.console import colorize, strip_colors from sphinx.util.logging import is_suppressed_warning, prefixed_warnings from sphinx.util.parallel import ParallelTasks @@ -360,15 +360,15 @@ def test_get_node_location_abspath(): # Ensure that node locations are reported as an absolute path, # even if the source attribute is a relative path. - relative_filename = os.path.join('relative', 'path.txt') - absolute_filename = osutil.abspath(relative_filename) + relative_filename = Path('relative', 'path.txt') + absolute_filename = relative_filename.resolve() n = nodes.Node() - n.source = relative_filename + n.source = str(relative_filename) location = logging.get_node_location(n) - assert location == absolute_filename + ':' + assert location == f'{absolute_filename}:' @pytest.mark.sphinx('html', testroot='root', confoverrides={'show_warning_types': True}) diff --git a/tests/test_util/test_util_rst.py b/tests/test_util/test_util_rst.py index bbe34f54b04..d6e83433af3 100644 --- a/tests/test_util/test_util_rst.py +++ b/tests/test_util/test_util_rst.py @@ -165,7 +165,7 @@ def test_textwidth(): def test_heading(): - env = Environment() + env = Environment(autoescape=True) env.extend(language=None) assert heading(env, 'Hello') == 'Hello\n=====' diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 1cc408a619e..314639efbe7 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -173,10 +173,7 @@ def test_restify_type_hints_containers(): ann_rst = restify(Tuple[str, ...]) assert ann_rst == ':py:class:`~typing.Tuple`\\ [:py:class:`str`, ...]' - if sys.version_info[:2] <= (3, 10): - assert restify(Tuple[()]) == ':py:class:`~typing.Tuple`\\ [()]' - else: - assert restify(Tuple[()]) == ':py:class:`~typing.Tuple`' + assert restify(Tuple[()]) == ':py:class:`~typing.Tuple`' assert restify(List[Dict[str, Tuple]]) == ( ':py:class:`~typing.List`\\ ' @@ -423,10 +420,9 @@ class X(t.TypedDict): assert restify(UnpackCompat['X'], 'fully-qualified-except-typing') == expect assert restify(UnpackCompat['X'], 'smart') == expect - if sys.version_info[:2] >= (3, 11): - expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]' - assert restify(t.Unpack['X'], 'fully-qualified-except-typing') == expect - assert restify(t.Unpack['X'], 'smart') == expect + expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]' + assert restify(t.Unpack['X'], 'fully-qualified-except-typing') == expect + assert restify(t.Unpack['X'], 'smart') == expect def test_restify_type_union_operator(): @@ -534,17 +530,9 @@ def test_stringify_type_hints_containers(): assert ann_str == 'typing.Tuple[str, ...]' assert stringify_annotation(Tuple[str, ...], 'smart') == '~typing.Tuple[str, ...]' - if sys.version_info[:2] <= (3, 10): - ann_str = stringify_annotation(Tuple[()], 'fully-qualified-except-typing') - assert ann_str == 'Tuple[()]' - assert stringify_annotation(Tuple[()], 'fully-qualified') == 'typing.Tuple[()]' - assert stringify_annotation(Tuple[()], 'smart') == '~typing.Tuple[()]' - else: - assert ( - stringify_annotation(Tuple[()], 'fully-qualified-except-typing') == 'Tuple' - ) - assert stringify_annotation(Tuple[()], 'fully-qualified') == 'typing.Tuple' - assert stringify_annotation(Tuple[()], 'smart') == '~typing.Tuple' + assert stringify_annotation(Tuple[()], 'fully-qualified-except-typing') == 'Tuple' + assert stringify_annotation(Tuple[()], 'fully-qualified') == 'typing.Tuple' + assert stringify_annotation(Tuple[()], 'smart') == '~typing.Tuple' ann_str = stringify_annotation( List[Dict[str, Tuple]], 'fully-qualified-except-typing' @@ -677,30 +665,13 @@ def test_stringify_Annotated(): def test_stringify_Unpack(): - from typing_extensions import Unpack as UnpackCompat - class X(t.TypedDict): x: int y: int label: str - if sys.version_info[:2] >= (3, 11): - # typing.Unpack is introduced in 3.11 but typing_extensions.Unpack only - # uses typing.Unpack in 3.12+, so the objects are not synchronised with - # each other, but we will assume that users use typing.Unpack. - import typing - - UnpackCompat = typing.Unpack # NoQA: F811 - assert stringify_annotation(UnpackCompat['X']) == 'Unpack[X]' - assert stringify_annotation(UnpackCompat['X'], 'smart') == '~typing.Unpack[X]' - else: - assert stringify_annotation(UnpackCompat['X']) == 'typing_extensions.Unpack[X]' - ann_str = stringify_annotation(UnpackCompat['X'], 'smart') - assert ann_str == '~typing_extensions.Unpack[X]' - - if sys.version_info[:2] >= (3, 11): - assert stringify_annotation(t.Unpack['X']) == 'Unpack[X]' - assert stringify_annotation(t.Unpack['X'], 'smart') == '~typing.Unpack[X]' + assert stringify_annotation(t.Unpack['X']) == 'Unpack[X]' + assert stringify_annotation(t.Unpack['X'], 'smart') == '~typing.Unpack[X]' def test_stringify_type_hints_string(): diff --git a/tests/test_versioning.py b/tests/test_versioning.py index f74b420fef7..14e7520db49 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -8,12 +8,12 @@ from sphinx.testing.util import SphinxTestApp from sphinx.versioning import add_uids, get_ratio, merge_doctrees -app = original = original_uids = None +original = original_uids = None @pytest.fixture(scope='module', autouse=True) def _setup_module(rootdir, sphinx_test_tempdir): - global app, original, original_uids + global original, original_uids # NoQA: PLW0603 srcdir = sphinx_test_tempdir / 'test-versioning' if not srcdir.exists(): shutil.copytree(rootdir / 'test-versioning', srcdir) diff --git a/tests/utils.py b/tests/utils.py index bec90a39772..ba4640aa33a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -67,7 +67,7 @@ def http_server( server_thread = server_cls(handler, port=port) server_thread.start() server_port = server_thread.server.server_port - assert port == 0 or server_port == port + assert port in {0, server_port} try: socket.create_connection(('localhost', server_port), timeout=0.5).close() yield server_thread.server # Connection has been confirmed possible; proceed. diff --git a/tox.ini b/tox.ini index 810cd46f9ec..572647c1196 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.2.0 -envlist = py{310,311,312,313} +envlist = py{311,312,313,314} [testenv] usedevelop = True @@ -19,7 +19,7 @@ passenv = BUILDER READTHEDOCS description = - py{310,311,312,313}: Run unit tests against {envname}. + py{311,312,313,314}: Run unit tests against {envname}. extras = test setenv =