diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6d5d145ca..03180df6dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] include: - os: macos-latest python-version: '3.13' diff --git a/.github/workflows/lower-bound-requirements.yml b/.github/workflows/lower-bound-requirements.yml index 190cec22a3..b4ac0bbd82 100644 --- a/.github/workflows/lower-bound-requirements.yml +++ b/.github/workflows/lower-bound-requirements.yml @@ -17,7 +17,7 @@ jobs: matrix: os: [ubuntu-latest] # minimum supported Python - python-version: ['3.8'] + python-version: ['3.9'] steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml index 10531df951..08dffa1347 100644 --- a/.github/workflows/release_tests.yml +++ b/.github/workflows/release_tests.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] include: - os: macos-latest python-version: '3.13' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 075396860f..ef188fe0b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,14 +55,15 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.13.0 # check the oldest and newest supported Pythons + # except skip python 3.9 for numpy, due to poor typing hooks: - &mypy id: mypy - name: mypy with Python 3.8 + name: mypy with Python 3.10 files: src additional_dependencies: ['numpy', 'types-tqdm', 'click', 'types-jsonpatch', 'types-pyyaml', 'types-jsonschema', 'importlib_metadata', 'packaging'] - args: ["--python-version=3.8"] + args: ["--python-version=3.10"] - <<: *mypy name: mypy with Python 3.13 args: ["--python-version=3.13"] diff --git a/pyproject.toml b/pyproject.toml index ba208ef848..afa53ffb7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dynamic = ["version"] description = "pure-Python HistFactory implementation with tensors and autodiff" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Lukas Heinrich", email = "lukas.heinrich@cern.ch" }, { name = "Matthew Feickert", email = "matthew.feickert@cern.ch" }, @@ -32,7 +32,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -44,7 +43,6 @@ classifiers = [ ] dependencies = [ "click>=8.0.0", # for console scripts - "importlib_resources>=1.4.0; python_version < '3.9'", # for resources in schema "jsonpatch>=1.15", "jsonschema>=4.15.0", # for utils "pyyaml>=5.1", # for parsing CLI equal-delimited options diff --git a/src/pyhf/contrib/utils.py b/src/pyhf/contrib/utils.py index 7fa9550242..a507fbc4b4 100644 --- a/src/pyhf/contrib/utils.py +++ b/src/pyhf/contrib/utils.py @@ -80,13 +80,7 @@ def download(archive_url, output_directory, force=False, compress=False): with open(output_directory, "wb") as archive: archive.write(response.content) else: - # Support for file-like objects for tarfile.is_tarfile was added - # in Python 3.9, so as pyhf is currently Python 3.8+ then can't - # do tarfile.is_tarfile(BytesIO(response.content)). - # Instead, just use a 'try except' block to determine if the - # archive is a valid tarfile. - # TODO: Simplify after pyhf is Python 3.9+ only - try: + if tarfile.is_tarfile(BytesIO(response.content)): # Use transparent compression to allow for .tar or .tar.gz with tarfile.open( mode="r:*", fileobj=BytesIO(response.content) @@ -97,13 +91,7 @@ def download(archive_url, output_directory, force=False, compress=False): archive.extractall(output_directory, filter="data") else: archive.extractall(output_directory) - except tarfile.ReadError: - if not zipfile.is_zipfile(BytesIO(response.content)): - raise exceptions.InvalidArchive( - f"The archive downloaded from {archive_url} is not a tarfile" - + " or a zipfile and so can not be opened as one." - ) - + elif zipfile.is_zipfile(BytesIO(response.content)): output_directory = Path(output_directory) if output_directory.exists(): rmtree(output_directory) @@ -129,6 +117,11 @@ def download(archive_url, output_directory, force=False, compress=False): # from creation time rmtree(output_directory) _tmp_path.replace(output_directory) + else: + raise exceptions.InvalidArchive( + f"The archive downloaded from {archive_url} is not a tarfile" + + " or a zipfile and so can not be opened as one." + ) except ModuleNotFoundError: log.error( diff --git a/src/pyhf/mixins.py b/src/pyhf/mixins.py index 0314188cc1..4855e95101 100644 --- a/src/pyhf/mixins.py +++ b/src/pyhf/mixins.py @@ -1,7 +1,8 @@ from __future__ import annotations import logging -from typing import Any, Sequence +from typing import Any +from collections.abc import Sequence from pyhf.typing import Channel diff --git a/src/pyhf/modifiers/staterror.py b/src/pyhf/modifiers/staterror.py index a6d6d499c5..bb4e9f3f3e 100644 --- a/src/pyhf/modifiers/staterror.py +++ b/src/pyhf/modifiers/staterror.py @@ -1,5 +1,4 @@ import logging -from typing import List import pyhf from pyhf import events @@ -10,7 +9,7 @@ log = logging.getLogger(__name__) -def required_parset(sigmas, fixed: List[bool]): +def required_parset(sigmas, fixed: list[bool]): n_parameters = len(sigmas) return { 'paramset_type': 'constrained_by_normal', diff --git a/src/pyhf/parameters/paramsets.py b/src/pyhf/parameters/paramsets.py index 2562c89305..3a59d4a1e8 100644 --- a/src/pyhf/parameters/paramsets.py +++ b/src/pyhf/parameters/paramsets.py @@ -1,5 +1,3 @@ -from typing import List - import pyhf __all__ = [ @@ -29,7 +27,7 @@ def __init__(self, **kwargs): ) @property - def suggested_fixed(self) -> List[bool]: + def suggested_fixed(self) -> list[bool]: if isinstance(self._suggested_fixed, bool): return [self._suggested_fixed] * self.n_parameters return self._suggested_fixed diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py index ca051d1652..b8cfe612bc 100644 --- a/src/pyhf/pdf.py +++ b/src/pyhf/pdf.py @@ -2,7 +2,7 @@ import copy import logging -from typing import List, Union +from typing import Union import pyhf.parameters import pyhf @@ -406,7 +406,7 @@ def param_set(self, name): """ return self.par_map[name]['paramset'] - def suggested_fixed(self) -> List[bool]: + def suggested_fixed(self) -> list[bool]: """ Identify the fixed parameters in the model. diff --git a/src/pyhf/readxml.py b/src/pyhf/readxml.py index a694dab292..52612ae082 100644 --- a/src/pyhf/readxml.py +++ b/src/pyhf/readxml.py @@ -4,16 +4,10 @@ from typing import ( IO, Callable, - Iterable, - List, - MutableMapping, - MutableSequence, - Sequence, - Set, - Tuple, Union, cast, ) +from collections.abc import Iterable, MutableMapping, MutableSequence, Sequence import xml.etree.ElementTree as ET from pathlib import Path @@ -46,8 +40,8 @@ log = logging.getLogger(__name__) -FileCacheType = MutableMapping[str, Tuple[Union[IO[str], IO[bytes]], Set[str]]] -MountPathType = Iterable[Tuple[Path, Path]] +FileCacheType = MutableMapping[str, tuple[Union[IO[str], IO[bytes]], set[str]]] +MountPathType = Iterable[tuple[Path, Path]] ResolverType = Callable[[str], Path] __FILECACHE__: FileCacheType = {} @@ -99,7 +93,7 @@ def extract_error(hist: uproot.behaviors.TH1.TH1) -> list[float]: """ variance = hist.variances() if hist.weighted else hist.to_numpy()[0] - return cast(List[float], np.sqrt(variance).tolist()) + return cast(list[float], np.sqrt(variance).tolist()) def import_root_histogram( diff --git a/src/pyhf/schema/loader.py b/src/pyhf/schema/loader.py index 920766c4dc..0f0001faef 100644 --- a/src/pyhf/schema/loader.py +++ b/src/pyhf/schema/loader.py @@ -1,15 +1,8 @@ from pathlib import Path -import sys import json import pyhf.exceptions from pyhf.schema import variables - -# importlib.resources.as_file wasn't added until Python 3.9 -# c.f. https://docs.python.org/3.9/library/importlib.html#importlib.resources.as_file -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources +from importlib import resources def load_schema(schema_id: str): diff --git a/src/pyhf/schema/validator.py b/src/pyhf/schema/validator.py index 2540a3d002..69230d7410 100644 --- a/src/pyhf/schema/validator.py +++ b/src/pyhf/schema/validator.py @@ -1,6 +1,7 @@ import numbers from pathlib import Path -from typing import Mapping, Union +from typing import Union +from collections.abc import Mapping import jsonschema diff --git a/src/pyhf/schema/variables.py b/src/pyhf/schema/variables.py index 80c0a0dd06..d02cc6b322 100644 --- a/src/pyhf/schema/variables.py +++ b/src/pyhf/schema/variables.py @@ -1,11 +1,5 @@ -import sys +from importlib import resources -# importlib.resources.as_file wasn't added until Python 3.9 -# c.f. https://docs.python.org/3.9/library/importlib.html#importlib.resources.as_file -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources schemas = resources.files('pyhf') / "schemas" SCHEMA_CACHE = {} diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index e843330bb3..2cc36545a2 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -3,7 +3,8 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Callable, Generic, Mapping, Sequence, TypeVar, Union +from typing import TYPE_CHECKING, Callable, Generic, TypeVar, Union +from collections.abc import Mapping, Sequence import numpy as np @@ -207,7 +208,8 @@ def conditional( def tolist(self, tensor_in: Tensor[T] | list[T]) -> list[T]: try: - return tensor_in.tolist() # type: ignore[union-attr,no-any-return] + # unused-ignore for [no-any-return] in python 3.9 + return tensor_in.tolist() # type: ignore[union-attr,no-any-return,unused-ignore] except AttributeError: if isinstance(tensor_in, list): return tensor_in @@ -654,4 +656,5 @@ def transpose(self, tensor_in: Tensor[T]) -> ArrayLike: .. versionadded:: 0.7.0 """ - return tensor_in.transpose() + # TODO: Casting needed for Python 3.10 mypy but not Python 3.13? + return cast(ArrayLike, tensor_in.transpose()) diff --git a/src/pyhf/typing.py b/src/pyhf/typing.py index 19dd36c485..f3e4e5784c 100644 --- a/src/pyhf/typing.py +++ b/src/pyhf/typing.py @@ -2,14 +2,12 @@ from typing import ( Any, Literal, - MutableSequence, Protocol, - Sequence, SupportsIndex, - Tuple, TypedDict, Union, ) +from collections.abc import MutableSequence, Sequence __all__ = ( "Channel", @@ -35,10 +33,9 @@ ) -# TODO: Switch to os.PathLike[str] once Python 3.8 support dropped -PathOrStr = Union[str, "os.PathLike[str]"] +PathOrStr = Union[str, os.PathLike[str]] -Shape = Tuple[int, ...] +Shape = tuple[int, ...] ShapeLike = Union[SupportsIndex, Sequence[SupportsIndex]] diff --git a/src/pyhf/utils.py b/src/pyhf/utils.py index c9ad5d0185..ea2fdfa99a 100644 --- a/src/pyhf/utils.py +++ b/src/pyhf/utils.py @@ -4,14 +4,7 @@ import hashlib from gettext import gettext -import sys - -# importlib.resources.as_file wasn't added until Python 3.9 -# c.f. https://docs.python.org/3.9/library/importlib.html#importlib.resources.as_file -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources +from importlib import resources __all__ = [ "EqDelimStringParamType", diff --git a/tests/contrib/test_viz.py b/tests/contrib/test_viz.py index f8f760931f..f7c0b29427 100644 --- a/tests/contrib/test_viz.py +++ b/tests/contrib/test_viz.py @@ -1,5 +1,4 @@ import json -import sys import matplotlib import matplotlib.pyplot as plt @@ -68,10 +67,6 @@ def test_plot_results(datadir): @pytest.mark.mpl_image_compare -@pytest.mark.xfail( - sys.version_info < (3, 8), - reason="baseline image generated with matplotlib v3.6.0 which is Python 3.8+", -) def test_plot_results_no_axis(datadir): data = json.load(datadir.joinpath("hypotest_results.json").open(encoding="utf-8"))