diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c3f5a729e..b96bbafd6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.10' - uses: pre-commit/action@v3.0.1 tests-core: @@ -23,12 +23,12 @@ jobs: fail-fast: false # Set on "false" to get the results of ALL builds matrix: os: ["ubuntu-latest"] - python-version: ["3.9", "3.12", "3.13"] + python-version: ["3.10", "3.12", "3.13"] sphinx-version: ["7.4", "8.2"] include: # corner cases for Windows - os: "windows-latest" - python-version: "3.9" + python-version: "3.10" sphinx-version: "7.4" - os: "windows-latest" python-version: "3.12" @@ -39,7 +39,7 @@ jobs: exclude: # Sphinx 8.2 only supports py3.11+ - os: "ubuntu-latest" - python-version: "3.9" + python-version: "3.10" sphinx-version: "8.2" steps: @@ -81,7 +81,7 @@ jobs: matrix: include: - os: "ubuntu-latest" - python-version: "3.9" + python-version: "3.10" sphinx-version: "7.4" - os: "ubuntu-latest" python-version: "3.13" diff --git a/docs/contributing.rst b/docs/contributing.rst index 25420f7e2..b500bf35d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -85,7 +85,7 @@ Or use tox (recommended): .. code-block:: bash - tox -e py39 + tox -e py310 Note some tests use `syrupy `__ to perform snapshot testing. These snapshots can be updated by running: diff --git a/pyproject.toml b/pyproject.toml index c9f4635b7..32eecab91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ classifiers = [ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', @@ -26,7 +25,7 @@ classifiers = [ 'Topic :: Utilities', 'Framework :: Sphinx :: Extension', ] -requires-python = ">=3.9,<4" +requires-python = ">=3.10,<4" dependencies = [ "sphinx>=7.4,<9", "requests-file~=2.1", # external links @@ -145,19 +144,19 @@ disable_error_code = ["no-redef"] legacy_tox_ini = """ [tox] -envlist = py39 +envlist = py10 [testenv] usedevelop = true -[testenv:py{39,310,311,312,313}] +[testenv:py{310,311,312,313}] extras = test test-parallel commands = pytest --ignore tests/benchmarks {posargs:tests} -[testenv:py{39,310,311,312,313}-benchmark] +[testenv:py{310,311,312,313}-benchmark] extras = test benchmark diff --git a/sphinx_needs/api/configuration.py b/sphinx_needs/api/configuration.py index d4a2bb86c..090d1109f 100644 --- a/sphinx_needs/api/configuration.py +++ b/sphinx_needs/api/configuration.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Callable +from collections.abc import Callable from sphinx.application import Sphinx from sphinx.util.logging import SphinxLoggerAdapter diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index 7caa8420e..3c245609c 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -1,8 +1,8 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping from dataclasses import MISSING, dataclass, field, fields -from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, TypedDict from docutils.parsers.rst import directives from sphinx.application import Sphinx diff --git a/sphinx_needs/debug.py b/sphinx_needs/debug.py index 8c847a15d..975d0e02d 100644 --- a/sphinx_needs/debug.py +++ b/sphinx_needs/debug.py @@ -8,11 +8,12 @@ import inspect import json import os.path +from collections.abc import Callable from datetime import datetime from functools import wraps from pathlib import Path from timeit import default_timer as timer # Used for timing measurements -from typing import Any, Callable, TypeVar +from typing import Any, TypeVar from jinja2 import Environment, PackageLoader, select_autoescape from sphinx.application import Sphinx diff --git a/sphinx_needs/directives/need.py b/sphinx_needs/directives/need.py index ac84d06d4..88c814990 100644 --- a/sphinx_needs/directives/need.py +++ b/sphinx_needs/directives/need.py @@ -1,8 +1,8 @@ from __future__ import annotations import re -from collections.abc import Sequence -from typing import Any, Callable +from collections.abc import Callable, Sequence +from typing import Any from docutils import nodes from sphinx.addnodes import desc_name, desc_signature diff --git a/sphinx_needs/directives/needbar.py b/sphinx_needs/directives/needbar.py index 833d1a03f..bb63ea5a5 100644 --- a/sphinx_needs/directives/needbar.py +++ b/sphinx_needs/directives/needbar.py @@ -390,7 +390,9 @@ def process_needbar( if current_needbar["stacked"]: # handle stacked bar - y_offset = [i + j for i, j in zip(y_offset, local_data_number[x])] + y_offset = [ + i + j for i, j in zip(y_offset, local_data_number[x], strict=False) + ] if current_needbar["show_sum"]: try: @@ -427,7 +429,8 @@ def process_needbar( matplotlib.pyplot.setp(bar_labels, rotation=int(sum_rotation)) centers = [ - (i + j) / 2.0 for i, j in zip(index[0], index[len(local_data_number) - 1]) + (i + j) / 2.0 + for i, j in zip(index[0], index[len(local_data_number) - 1], strict=False) ] if not current_needbar["horizontal"]: # We want to support even older version of matplotlib, which do not support axes.set_xticks(labels) diff --git a/sphinx_needs/directives/needextend.py b/sphinx_needs/directives/needextend.py index b4e88be27..c9fb100eb 100644 --- a/sphinx_needs/directives/needextend.py +++ b/sphinx_needs/directives/needextend.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import Any, Callable +from collections.abc import Callable, Sequence +from typing import Any from docutils import nodes from docutils.parsers.rst import directives diff --git a/sphinx_needs/directives/needflow/_graphviz.py b/sphinx_needs/directives/needflow/_graphviz.py index 83b658e2b..ed8d9a256 100644 --- a/sphinx_needs/directives/needflow/_graphviz.py +++ b/sphinx_needs/directives/needflow/_graphviz.py @@ -2,8 +2,9 @@ import html import textwrap +from collections.abc import Callable from functools import cache -from typing import Callable, Literal, TypedDict +from typing import Literal, TypedDict from urllib.parse import urlparse from docutils import nodes diff --git a/sphinx_needs/directives/needservice.py b/sphinx_needs/directives/needservice.py index 3126fc46e..42fc9bda8 100644 --- a/sphinx_needs/directives/needservice.py +++ b/sphinx_needs/directives/needservice.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import Any, Callable +from collections.abc import Callable, Sequence +from typing import Any from docutils import nodes from docutils.parsers.rst import directives diff --git a/sphinx_needs/directives/needtable.py b/sphinx_needs/directives/needtable.py index d006499fd..6583fdf14 100644 --- a/sphinx_needs/directives/needtable.py +++ b/sphinx_needs/directives/needtable.py @@ -1,8 +1,8 @@ from __future__ import annotations import re -from collections.abc import Sequence -from typing import Any, Callable +from collections.abc import Callable, Sequence +from typing import Any from docutils import nodes from docutils.parsers.rst import directives diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index a01179611..ccc28703e 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -8,11 +8,11 @@ import ast import json import re -from collections.abc import Iterable +from collections.abc import Callable, Iterable from pathlib import Path from timeit import default_timer as timer from types import CodeType -from typing import Any, Callable, TypedDict, overload +from typing import Any, TypedDict, overload from docutils import nodes from docutils.parsers.rst import directives @@ -318,8 +318,8 @@ def _analyze_and_apply_expr( :returns: the needs (potentially filtered), and a boolean denoting if it still requires python eval filtering """ - if isinstance(expr, (ast.Str, ast.Constant)): - if isinstance(expr.s, (str, bool)): + if isinstance(expr, ast.Str | ast.Constant): + if isinstance(expr.s, str | bool): # "value" / True / False return needs if expr.s else needs.filter_ids([]), False @@ -335,13 +335,13 @@ def _analyze_and_apply_expr( if ( isinstance(expr.left, ast.Name) and len(expr.comparators) == 1 - and isinstance(expr.comparators[0], (ast.Str, ast.Constant)) + and isinstance(expr.comparators[0], ast.Str | ast.Constant) ): # x == "value" field = expr.left.id value = expr.comparators[0].s elif ( - isinstance(expr.left, (ast.Str, ast.Constant)) + isinstance(expr.left, ast.Str | ast.Constant) and len(expr.comparators) == 1 and isinstance(expr.comparators[0], ast.Name) ): @@ -369,9 +369,9 @@ def _analyze_and_apply_expr( if ( isinstance(expr.left, ast.Name) and len(expr.comparators) == 1 - and isinstance(expr.comparators[0], (ast.List, ast.Tuple, ast.Set)) + and isinstance(expr.comparators[0], ast.List | ast.Tuple | ast.Set) and all( - isinstance(elt, (ast.Str, ast.Constant)) + isinstance(elt, ast.Str | ast.Constant) for elt in expr.comparators[0].elts ) ): @@ -386,7 +386,7 @@ def _analyze_and_apply_expr( # type in ["a", "b", ...] return needs.filter_types(values), False elif ( - isinstance(expr.left, (ast.Str, ast.Constant)) + isinstance(expr.left, ast.Str | ast.Constant) and len(expr.comparators) == 1 and isinstance(expr.comparators[0], ast.Name) and expr.comparators[0].id == "tags" diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index 525421f7b..f5af5b0a8 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -112,7 +112,9 @@ def execute_func( ) return "??" - if func_return is not None and not isinstance(func_return, (str, int, float, list)): + if func_return is not None and not isinstance( + func_return, str | int | float | list + ): log_warning( logger, f"Return value of function {func_name!r} is of type {type(func_return)}. Allowed are str, int, float, list", @@ -122,7 +124,7 @@ def execute_func( return "??" if isinstance(func_return, list): for i, element in enumerate(func_return): - if not isinstance(element, (str, int, float)): + if not isinstance(element, str | int | float): log_warning( logger, f"Return value item {i} of function {func_name!r} is of type {type(element)}. Allowed are str, int, float", @@ -204,7 +206,7 @@ def find_and_replace_node_content( return node else: for child in node.children: - if isinstance(child, (nodes.literal_block, nodes.literal, Need)): + if isinstance(child, nodes.literal_block | nodes.literal | Need): # Do not parse literal blocks or nested needs new_children.append(child) continue @@ -253,7 +255,7 @@ def resolve_dynamic_values(needs: NeedsMutable, app: Sphinx) -> None: for need_option in need: if need_option not in allowed_fields: continue - if not isinstance(need[need_option], (list, set)): + if not isinstance(need[need_option], list | set): func_call: str | None = "init" while func_call: try: @@ -314,7 +316,7 @@ def resolve_dynamic_values(needs: NeedsMutable, app: Sphinx) -> None: ) if func_call is None: new_values.append(element) - elif isinstance(func_return, (list, set)): + elif isinstance(func_return, list | set): new_values += func_return else: new_values += [func_return] @@ -370,7 +372,7 @@ def resolve_variants_options( for var_option in variants_options: if ( var_option in need - and isinstance(need[var_option], (str, list, tuple, set)) + and isinstance(need[var_option], str | list | tuple | set) and ( result := match_variants( need[var_option], @@ -476,7 +478,7 @@ def _analyze_func_string( for arg in func_call.args: if isinstance(arg, ast.Num): func_args.append(arg.n) - elif isinstance(arg, (ast.Str, ast.BoolOp)): + elif isinstance(arg, ast.Str | ast.BoolOp): func_args.append(arg.s) # type: ignore elif isinstance(arg, ast.List): arg_list: list[Any] = [] diff --git a/sphinx_needs/layout.py b/sphinx_needs/layout.py index 1009ccad9..63e570aa9 100644 --- a/sphinx_needs/layout.py +++ b/sphinx_needs/layout.py @@ -9,11 +9,11 @@ import os import re import uuid +from collections.abc import Callable from contextlib import suppress from functools import lru_cache from optparse import Values from pathlib import Path -from typing import Callable from urllib.parse import urlparse import requests diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index 863243fbb..7eab592c5 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -1,9 +1,10 @@ from __future__ import annotations import contextlib +from collections.abc import Callable from pathlib import Path from timeit import default_timer as timer # Used for timing measurements -from typing import Any, Callable, Literal +from typing import Any, Literal from docutils import nodes from docutils.parsers.rst import directives @@ -782,9 +783,9 @@ def _gather_field_defaults( k: v for k, v in value.items() if k in {"predicates", "default"} } if "predicates" in single_default and ( - not isinstance(single_default["predicates"], (list, tuple)) + not isinstance(single_default["predicates"], list | tuple) or not all( - isinstance(x, (list, tuple)) + isinstance(x, list | tuple) and len(x) == 2 and isinstance(x[0], str) for x in single_default["predicates"] @@ -798,9 +799,9 @@ def _gather_field_defaults( ) continue elif ( - isinstance(value, (list, tuple)) + isinstance(value, list | tuple) and len(value) > 0 - and all(isinstance(x, (list, tuple)) for x in value) + and all(isinstance(x, list | tuple) for x in value) ): old_format = True single_default = {"predicates": []} @@ -830,7 +831,7 @@ def _gather_field_defaults( "config", None, ) - elif isinstance(value, (list, tuple)): + elif isinstance(value, list | tuple): old_format = True if len(value) == 2: # single (value, predicate) pair diff --git a/sphinx_needs/roles/need_ref.py b/sphinx_needs/roles/need_ref.py index 118b1e317..3e3c26d70 100644 --- a/sphinx_needs/roles/need_ref.py +++ b/sphinx_needs/roles/need_ref.py @@ -50,7 +50,7 @@ def value_to_string(value: Any) -> str: return value elif isinstance(value, dict): return ";".join([str(i) for i in value.items()]) - elif isinstance(value, (Iterable, list, tuple)): + elif isinstance(value, Iterable | list | tuple): return ";".join([str(i) for i in value]) return str(value) diff --git a/sphinx_needs/services/open_needs.py b/sphinx_needs/services/open_needs.py index 7c312413e..23cc0b77c 100644 --- a/sphinx_needs/services/open_needs.py +++ b/sphinx_needs/services/open_needs.py @@ -142,7 +142,7 @@ def _extract_data( for item in data: extra_data = {} for name, selector in self.extra_data.items(): - if not isinstance(selector, (tuple, list, str)): + if not isinstance(selector, tuple | list | str): raise InvalidConfigException( f"Given selector for {name} of extra_data must be a list or tuple. " f'Got {type(selector)} with value "{selector}"' @@ -172,7 +172,7 @@ def _extract_data( need_values = {} for name, selector in self.mappings.items(): - if not isinstance(selector, (tuple, list, str)): + if not isinstance(selector, tuple | list | str): raise InvalidConfigException( f"Given selector for {name} of mapping must be a list or tuple. " f'Got {type(selector)} with value "{selector}"' @@ -186,7 +186,7 @@ def _extract_data( need_values[name] = selector else: value = dict_get(item, selector) - if isinstance(value, (tuple, list)): + if isinstance(value, tuple | list): if name == "links": # Add a prefix to the referenced link if it is an ID of a need object in # the data retrieved from the Open Needs Server or don't add prefix diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index ce577744e..2934118f4 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -5,9 +5,10 @@ import operator import os import re +from collections.abc import Callable from dataclasses import dataclass from functools import lru_cache, reduce, wraps -from typing import TYPE_CHECKING, Any, Callable, Protocol, TypeVar +from typing import TYPE_CHECKING, Any, Protocol, TypeVar from urllib.parse import urlparse from docutils import nodes @@ -98,7 +99,7 @@ def row_col_maker( if need_key in need_info and need_info[need_key] is not None: # type: ignore[literal-required] value = need_info[need_key] # type: ignore[literal-required] - if isinstance(value, (list, set)): + if isinstance(value, list | set): data = value elif isinstance(value, str) and need_key in needs_string_links_option: data = re.split(r",|;", value) @@ -530,7 +531,7 @@ def match_variants( for i in options_list if i not in (None, ";", "", " ") ] - elif isinstance(options, (list, set, tuple)): + elif isinstance(options, list | set | tuple): options_list = [str(opt) for opt in options] else: raise TypeError( diff --git a/sphinx_needs/views.py b/sphinx_needs/views.py index 43ab0d08b..31059a6c7 100644 --- a/sphinx_needs/views.py +++ b/sphinx_needs/views.py @@ -2,13 +2,13 @@ from collections.abc import Iterable, Iterator, Mapping from itertools import chain -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: from sphinx_needs.data import NeedsInfoType -_IdSet = list[tuple[str, Optional[str]]] +_IdSet = list[tuple[str, str | None]] """Set of (need, part) ids."""