From 8320ea435c99a8f005739e5d6a81b1fdf04a6be5 Mon Sep 17 00:00:00 2001 From: barrust Date: Thu, 29 Jan 2026 20:33:18 -0500 Subject: [PATCH 1/2] drop python 3.9 support --- .github/workflows/python-package.yml | 2 +- probables/blooms/bloom.py | 52 +++++++-------- probables/blooms/countingbloom.py | 13 ++-- probables/blooms/expandingbloom.py | 29 ++++---- probables/countminsketch/countminsketch.py | 77 +++++++++++----------- probables/cuckoo/countingcuckoo.py | 22 +++---- probables/cuckoo/cuckoo.py | 25 ++++--- probables/hashes.py | 8 +-- probables/quotientfilter/quotientfilter.py | 8 +-- probables/utilities.py | 14 ++-- pyproject.toml | 7 +- tests/cuckoo_test.py | 3 +- tests/utilities.py | 3 +- 13 files changed, 127 insertions(+), 136 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6729f1e..c8032c6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v6 diff --git a/probables/blooms/bloom.py b/probables/blooms/bloom.py index 921e647..02106f2 100644 --- a/probables/blooms/bloom.py +++ b/probables/blooms/bloom.py @@ -29,7 +29,7 @@ def _verify_not_type_mismatch(second: SimpleBloomT) -> bool: """verify that there is not a type mismatch""" - return isinstance(second, (BloomFilter, BloomFilterOnDisk)) + return isinstance(second, BloomFilter | BloomFilterOnDisk) class BloomFilter: @@ -68,11 +68,11 @@ class BloomFilter: def __init__( self, - est_elements: Union[int, None] = None, - false_positive_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hex_string: Union[str, None] = None, - hash_function: Union[HashFuncT, None] = None, + est_elements: int | None = None, + false_positive_rate: float | None = None, + filepath: str | Path | None = None, + hex_string: str | None = None, + hash_function: HashFuncT | None = None, ): # set some things up self._on_disk = False @@ -110,7 +110,7 @@ def _load_init(self, filepath, hash_function, hex_string, est_elements, false_po _FPR_STRUCT = Struct("f") _IMPT_STRUCT = Struct("B") - def __contains__(self, key: KeyT) -> Union[int, bool]: + def __contains__(self, key: KeyT) -> int | bool: """setup the `in` keyword""" return self.check(key) @@ -220,7 +220,7 @@ def clear(self) -> None: for idx in range(self._bloom_length): self._bloom[idx] = 0 - def hashes(self, key: KeyT, depth: Union[int, None] = None) -> HashResultsT: + def hashes(self, key: KeyT, depth: int | None = None) -> HashResultsT: """Return the hashes based on the provided key Args: @@ -284,12 +284,12 @@ def export_hex(self) -> str: bytes_string = hexlify(bytearray(self._bloom[: self.bloom_length])) + hexlify(footer_bytes) return str(bytes_string, "utf-8") - def export(self, file: Union[Path, str, IOBase, mmap]) -> None: + def export(self, file: Path | str | IOBase | mmap) -> None: """Export the Bloom Filter to disk Args: file (str): The file or filepath to which the Bloom Filter will be written.""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with open(file, "wb") as filepointer: self.export(filepointer) @@ -303,7 +303,7 @@ def export(self, file: Union[Path, str, IOBase, mmap]) -> None: ) ) - def export_c_header(self, filename: Union[str, Path]) -> None: + def export_c_header(self, filename: str | Path) -> None: """Export the Bloom Filter to disk as a C header file. Args: @@ -322,7 +322,7 @@ def export_c_header(self, filename: Union[str, Path]) -> None: print("const unsigned char bloom[] = {", *data, "};", sep="\n", file=file) @classmethod - def frombytes(cls, b: ByteString, hash_function: Union[HashFuncT, None] = None) -> "BloomFilter": + def frombytes(cls, b: ByteString, hash_function: HashFuncT | None = None) -> "BloomFilter": """ Args: b (ByteString): The bytes to load as a Bloom Filter @@ -488,7 +488,7 @@ def _set_values( fpr: float, n_hashes: int, n_bits: int, - hash_func: Union[HashFuncT, None], + hash_func: HashFuncT | None, ) -> None: self._est_elements = est_els self._fpr = fpr @@ -501,7 +501,7 @@ def _set_values( self._number_hashes = n_hashes self._num_bits = n_bits - def _load_hex(self, hex_string: str, hash_function: Union[HashFuncT, None] = None) -> None: + def _load_hex(self, hex_string: str, hash_function: HashFuncT | None = None) -> None: """placeholder for loading from hex string""" offset = self._FOOTER_STRUCT_BE.size * 2 est_els, els_added, fpr, n_hashes, n_bits = self._parse_footer( @@ -513,11 +513,11 @@ def _load_hex(self, hex_string: str, hash_function: Union[HashFuncT, None] = Non def _load( self, - file: Union[Path, str, IOBase, mmap, ByteString], - hash_function: Union[HashFuncT, None] = None, + file: Path | str | IOBase | mmap | ByteString, + hash_function: HashFuncT | None = None, ) -> None: """load the Bloom Filter from file or bytes""" - if not isinstance(file, (IOBase, mmap, bytes, bytearray, memoryview)): + if not isinstance(file, IOBase | mmap | bytes | bytearray | memoryview): file = resolve_path(file) with MMap(file) as filepointer: self._load(filepointer, hash_function) @@ -594,15 +594,15 @@ class BloomFilterOnDisk(BloomFilter): def __init__( self, - filepath: Union[str, Path], - est_elements: Union[int, None] = None, - false_positive_rate: Union[float, None] = None, - hex_string: Union[str, None] = None, - hash_function: Union[HashFuncT, None] = None, + filepath: str | Path, + est_elements: int | None = None, + false_positive_rate: float | None = None, + hex_string: str | None = None, + hash_function: HashFuncT | None = None, ) -> None: # set some things up self._filepath = resolve_path(filepath) - self.__file_pointer: Union[BufferedRandom, None] = None + self.__file_pointer: BufferedRandom | None = None super().__init__(est_elements, false_positive_rate, filepath, hex_string, hash_function) def _load_init(self, filepath, hash_function, hex_string, est_elements, false_positive_rate): @@ -641,7 +641,7 @@ def close(self) -> None: self.__file_pointer.close() self.__file_pointer = None - def export(self, file: Union[str, Path]) -> None: # type: ignore + def export(self, file: str | Path) -> None: # type: ignore """Export to disk if a different location Args: @@ -653,7 +653,7 @@ def export(self, file: Union[str, Path]) -> None: # type: ignore copyfile(self._filepath, str(file)) # otherwise, nothing to do! - def _load(self, file: Union[str, Path], hash_function: Union[HashFuncT, None] = None): # type: ignore + def _load(self, file: str | Path, hash_function: HashFuncT | None = None): # type: ignore """load the Bloom Filter on disk""" # read the file, set the optimal params # mmap everything @@ -675,7 +675,7 @@ def add_alt(self, hashes: HashResultsT) -> None: self.__update() @classmethod - def frombytes(cls, b: ByteString, hash_function: Union[HashFuncT, None] = None) -> "BloomFilterOnDisk": + def frombytes(cls, b: ByteString, hash_function: HashFuncT | None = None) -> "BloomFilterOnDisk": """ Raises: NotSupportedError """ diff --git a/probables/blooms/countingbloom.py b/probables/blooms/countingbloom.py index 99556a6..89318ee 100644 --- a/probables/blooms/countingbloom.py +++ b/probables/blooms/countingbloom.py @@ -8,7 +8,6 @@ from collections.abc import ByteString from pathlib import Path from struct import Struct -from typing import Union from probables.blooms.bloom import BloomFilter from probables.constants import UINT32_T_MAX, UINT64_T_MAX @@ -48,11 +47,11 @@ class CountingBloomFilter(BloomFilter): def __init__( self, - est_elements: Union[int, None] = None, - false_positive_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hex_string: Union[str, None] = None, - hash_function: Union[HashFuncT, None] = None, + est_elements: int | None = None, + false_positive_rate: float | None = None, + filepath: str | Path | None = None, + hex_string: str | None = None, + hash_function: HashFuncT | None = None, ) -> None: """setup the basic values needed""" self._filepath = None @@ -81,7 +80,7 @@ def _load_init(self, filepath, hash_function, hex_string, est_elements, false_po _IMPT_STRUCT = Struct("I") @classmethod - def frombytes(cls, b: ByteString, hash_function: Union[HashFuncT, None] = None) -> "CountingBloomFilter": + def frombytes(cls, b: ByteString, hash_function: HashFuncT | None = None) -> "CountingBloomFilter": """ Args: b (ByteString): the bytes to load as a Counting Bloom Filter diff --git a/probables/blooms/expandingbloom.py b/probables/blooms/expandingbloom.py index ef9e765..28931f4 100644 --- a/probables/blooms/expandingbloom.py +++ b/probables/blooms/expandingbloom.py @@ -10,7 +10,6 @@ from mmap import mmap from pathlib import Path from struct import Struct -from typing import Union from probables.blooms.bloom import BloomFilter from probables.exceptions import RotatingBloomFilterError @@ -46,10 +45,10 @@ class ExpandingBloomFilter: def __init__( self, - est_elements: Union[int, None] = None, - false_positive_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + est_elements: int | None = None, + false_positive_rate: float | None = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ): """initialize""" self._blooms = [] # type: ignore @@ -74,7 +73,7 @@ def __init__( _BLOOM_ELEMENT_SIZE = Struct("B").size @classmethod - def frombytes(cls, b: ByteString, hash_function: Union[HashFuncT, None] = None) -> "ExpandingBloomFilter": + def frombytes(cls, b: ByteString, hash_function: HashFuncT | None = None) -> "ExpandingBloomFilter": """ Args: b (ByteString): The bytes to load as a Expanding Bloom Filter @@ -183,12 +182,12 @@ def __check_for_growth(self): if self._blooms[-1].elements_added >= self.__est_elements: self.__add_bloom_filter() - def export(self, file: Union[Path, str, IOBase, mmap]) -> None: + def export(self, file: Path | str | IOBase | mmap) -> None: """Export an expanding Bloom Filter, or subclass, to disk Args: filepath (str): The path to the file to import""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with open(file, "wb") as filepointer: self.export(filepointer) # type:ignore @@ -207,9 +206,9 @@ def export(self, file: Union[Path, str, IOBase, mmap]) -> None: ) ) - def __load(self, file: Union[Path, str, IOBase, mmap]): + def __load(self, file: Path | str | IOBase | mmap): """load a file""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with MMap(file) as filepointer: self.__load(filepointer) @@ -272,11 +271,11 @@ class RotatingBloomFilter(ExpandingBloomFilter): def __init__( self, - est_elements: Union[int, None] = None, - false_positive_rate: Union[float, None] = None, + est_elements: int | None = None, + false_positive_rate: float | None = None, max_queue_size: int = 10, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ) -> None: """initialize""" super().__init__( @@ -289,7 +288,7 @@ def __init__( @classmethod def frombytes( # type:ignore - cls, b: ByteString, max_queue_size: int, hash_function: Union[HashFuncT, None] = None + cls, b: ByteString, max_queue_size: int, hash_function: HashFuncT | None = None ) -> "RotatingBloomFilter": """ Args: diff --git a/probables/countminsketch/countminsketch.py b/probables/countminsketch/countminsketch.py index 98a5611..db56027 100644 --- a/probables/countminsketch/countminsketch.py +++ b/probables/countminsketch/countminsketch.py @@ -12,7 +12,6 @@ from numbers import Number from pathlib import Path from struct import Struct -from typing import Union from probables.constants import INT32_T_MAX, INT32_T_MIN, INT64_T_MAX, INT64_T_MIN from probables.exceptions import CountMinSketchError, InitializationError, NotSupportedError @@ -59,12 +58,12 @@ class CountMinSketch: def __init__( self, - width: Union[int, None] = None, - depth: Union[int, None] = None, - confidence: Union[float, None] = None, - error_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + width: int | None = None, + depth: int | None = None, + confidence: float | None = None, + error_rate: float | None = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ) -> None: """default initilization function""" # default values @@ -152,7 +151,7 @@ def __bytes__(self) -> bytes: return f.getvalue() @classmethod - def frombytes(cls, b: ByteString, hash_function: Union[HashFuncT, None] = None) -> "CountMinSketch": + def frombytes(cls, b: ByteString, hash_function: HashFuncT | None = None) -> "CountMinSketch": """ Args: b (ByteString): The bytes to load as a Count-Min Sketch @@ -244,7 +243,7 @@ def clear(self) -> None: for i, _ in enumerate(self._bins): self._bins[i] = 0 - def hashes(self, key: KeyT, depth: Union[int, None] = None) -> HashResultsT: + def hashes(self, key: KeyT, depth: int | None = None) -> HashResultsT: """Return the hashes based on the provided key Args: @@ -340,12 +339,12 @@ def check_alt(self, hashes: HashResultsT) -> int: bins = [(val % self.width) + (i * self.width) for i, val in enumerate(hashes)] return self.__query_method(sorted([self._bins[i] for i in bins])) - def export(self, file: Union[Path, str, IOBase, mmap]) -> None: + def export(self, file: Path | str | IOBase | mmap) -> None: """Export the count-min sketch to disk Args: filename (str): The filename to which the count-min sketch will be written.""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with open(file, "wb") as filepointer: self.export(filepointer) # type: ignore @@ -399,9 +398,9 @@ def join(self, second: "CountMinSketch") -> None: elif self.elements_added < INT64_T_MIN: self.__elements_added = INT64_T_MIN - def __load(self, file: Union[Path, str, IOBase, mmap]): + def __load(self, file: Path | str | IOBase | mmap): """load the count-min sketch from file""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with MMap(file) as filepointer: self.__load(filepointer) @@ -481,12 +480,12 @@ class CountMeanSketch(CountMinSketch): def __init__( self, - width: Union[int, None] = None, - depth: Union[int, None] = None, - confidence: Union[float, None] = None, - error_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + width: int | None = None, + depth: int | None = None, + confidence: float | None = None, + error_rate: float | None = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ) -> None: super().__init__(width, depth, confidence, error_rate, filepath, hash_function) self.query_type = "mean" @@ -519,12 +518,12 @@ class CountMeanMinSketch(CountMinSketch): def __init__( self, - width: Union[int, None] = None, - depth: Union[int, None] = None, - confidence: Union[float, None] = None, - error_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + width: int | None = None, + depth: int | None = None, + confidence: float | None = None, + error_rate: float | None = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ) -> None: super().__init__(width, depth, confidence, error_rate, filepath, hash_function) self.query_type = "mean-min" @@ -560,12 +559,12 @@ class HeavyHitters(CountMinSketch): def __init__( self, num_hitters: int = 100, - width: Union[int, None] = None, - depth: Union[int, None] = None, - confidence: Union[float, None] = None, - error_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + width: int | None = None, + depth: int | None = None, + confidence: float | None = None, + error_rate: float | None = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ) -> None: super().__init__(width, depth, confidence, error_rate, filepath, hash_function) self.__top_x = {} # type: ignore @@ -575,7 +574,7 @@ def __init__( @classmethod def frombytes( # type: ignore - cls, b: ByteString, num_hitters: int = 100, hash_function: Union[HashFuncT, None] = None + cls, b: ByteString, num_hitters: int = 100, hash_function: HashFuncT | None = None ) -> "HeavyHitters": """ Args: @@ -721,12 +720,12 @@ class StreamThreshold(CountMinSketch): def __init__( self, threshold: int = 100, - width: Union[int, None] = None, - depth: Union[int, None] = None, - confidence: Union[float, None] = None, - error_rate: Union[float, None] = None, - filepath: Union[str, Path, None] = None, - hash_function: Union[HashFuncT, None] = None, + width: int | None = None, + depth: int | None = None, + confidence: float | None = None, + error_rate: float | None = None, + filepath: str | Path | None = None, + hash_function: HashFuncT | None = None, ) -> None: super().__init__(width, depth, confidence, error_rate, filepath, hash_function) self.__threshold = threshold @@ -734,7 +733,7 @@ def __init__( @classmethod def frombytes( # type: ignore - cls, b: ByteString, threshold: int = 100, hash_function: Union[HashFuncT, None] = None + cls, b: ByteString, threshold: int = 100, hash_function: HashFuncT | None = None ) -> "StreamThreshold": """ Args: diff --git a/probables/cuckoo/countingcuckoo.py b/probables/cuckoo/countingcuckoo.py index 4fb52c0..f3df32c 100644 --- a/probables/cuckoo/countingcuckoo.py +++ b/probables/cuckoo/countingcuckoo.py @@ -44,8 +44,8 @@ def __init__( expansion_rate: int = 2, auto_expand: bool = True, finger_size: int = 4, - filepath: Union[str, Path, None] = None, - hash_function: Union[SimpleHashT, None] = None, + filepath: str | Path | None = None, + hash_function: SimpleHashT | None = None, ) -> None: """setup the data structure""" self.__unique_elements = 0 @@ -72,7 +72,7 @@ def init_error_rate( max_swaps: int = 500, expansion_rate: int = 2, auto_expand: bool = True, - hash_function: Union[SimpleHashT, None] = None, + hash_function: SimpleHashT | None = None, ): """Initialize a simple Cuckoo Filter based on error rate @@ -98,9 +98,7 @@ def init_error_rate( return cku @classmethod - def load_error_rate( - cls, error_rate: float, filepath: Union[str, Path], hash_function: Union[SimpleHashT, None] = None - ): + def load_error_rate(cls, error_rate: float, filepath: str | Path, hash_function: SimpleHashT | None = None): """Initialize a previously exported Cuckoo Filter based on error rate Args: @@ -118,7 +116,7 @@ def load_error_rate( @classmethod def frombytes( - cls, b: ByteString, error_rate: Union[float, None] = None, hash_function: Union[SimpleHashT, None] = None + cls, b: ByteString, error_rate: float | None = None, hash_function: SimpleHashT | None = None ) -> "CountingCuckooFilter": """ Args: @@ -215,12 +213,12 @@ def expand(self): """Expand the cuckoo filter""" self._expand_logic(None) - def export(self, file: Union[Path, str, IOBase, mmap]) -> None: + def export(self, file: Path | str | IOBase | mmap) -> None: """Export cuckoo filter to file Args: file (str): Path to file to export""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with open(file, "wb") as filepointer: self.export(filepointer) # type:ignore @@ -266,7 +264,7 @@ def _insert_fingerprint_alt( # if we got here we have an error... we might need to know what is left return prv_bin - def _check_if_present(self, idx_1: int, idx_2: int, fingerprint: int) -> Union[int, None]: + def _check_if_present(self, idx_1: int, idx_2: int, fingerprint: int) -> int | None: """wrapper for checking if fingerprint is already inserted""" if fingerprint in [x.finger for x in self.buckets[idx_1]]: return idx_1 @@ -274,9 +272,9 @@ def _check_if_present(self, idx_1: int, idx_2: int, fingerprint: int) -> Union[i return idx_2 return None - def _load(self, file: Union[Path, str, IOBase, mmap, bytes, ByteString]) -> None: + def _load(self, file: Path | str | IOBase | mmap | bytes | ByteString) -> None: """load a cuckoo filter from file""" - if not isinstance(file, (IOBase, mmap, bytes, bytearray, memoryview)): + if not isinstance(file, IOBase | mmap | bytes | bytearray | memoryview): file = resolve_path(file) with MMap(file) as filepointer: self._load(filepointer) diff --git a/probables/cuckoo/cuckoo.py b/probables/cuckoo/cuckoo.py index 1505da2..0625b57 100644 --- a/probables/cuckoo/cuckoo.py +++ b/probables/cuckoo/cuckoo.py @@ -12,7 +12,6 @@ from numbers import Number from pathlib import Path from struct import Struct -from typing import Union from probables.exceptions import CuckooFilterFullError, InitializationError from probables.hashes import KeyT, SimpleHashT, fnv_1a @@ -57,8 +56,8 @@ def __init__( expansion_rate: int = 2, auto_expand: bool = True, finger_size: int = 4, - filepath: Union[str, Path, None] = None, - hash_function: Union[SimpleHashT, None] = None, + filepath: str | Path | None = None, + hash_function: SimpleHashT | None = None, ): """setup the data structure""" valid_prms = ( @@ -109,7 +108,7 @@ def init_error_rate( max_swaps: int = 500, expansion_rate: int = 2, auto_expand: bool = True, - hash_function: Union[SimpleHashT, None] = None, + hash_function: SimpleHashT | None = None, ): """Initialize a simple Cuckoo Filter based on error rate @@ -139,8 +138,8 @@ def init_error_rate( def load_error_rate( cls, error_rate: float, - filepath: Union[str, Path], - hash_function: Union[SimpleHashT, None] = None, + filepath: str | Path, + hash_function: SimpleHashT | None = None, ): """Initialize a previously exported Cuckoo Filter based on error rate @@ -159,8 +158,8 @@ def load_error_rate( def frombytes( cls, b: ByteString, - error_rate: Union[float, None] = None, - hash_function: Union[SimpleHashT, None] = None, + error_rate: float | None = None, + hash_function: SimpleHashT | None = None, ) -> "CuckooFilter": """ Args: @@ -330,13 +329,13 @@ def remove(self, key: KeyT) -> bool: self._inserted_elements -= 1 return True - def export(self, file: Union[Path, str, IOBase, mmap]) -> None: + def export(self, file: Path | str | IOBase | mmap) -> None: """Export cuckoo filter to file Args: file: Path to file to export""" - if not isinstance(file, (IOBase, mmap)): + if not isinstance(file, IOBase | mmap): file = resolve_path(file) with open(file, "wb") as filepointer: self.export(filepointer) # type:ignore @@ -392,9 +391,9 @@ def _insert_fingerprint(self, fingerprint, idx_1, idx_2): # if we got here we have an error... we might need to know what is left return fingerprint - def _load(self, file: Union[Path, str, IOBase, mmap, bytes]) -> None: + def _load(self, file: Path | str | IOBase | mmap | bytes) -> None: """load a cuckoo filter from file""" - if not isinstance(file, (IOBase, mmap, bytes)): + if not isinstance(file, IOBase | mmap | bytes): file = resolve_path(file) with MMap(file) as filepointer: self._load(filepointer) @@ -431,7 +430,7 @@ def _parse_bucket(self, d: ByteString) -> array: self._inserted_elements += len(bucket) return bucket - def _set_error_rate(self, error_rate: Union[float, None]) -> None: + def _set_error_rate(self, error_rate: float | None) -> None: """set error rate correctly""" # if error rate is provided, use it if error_rate is not None: diff --git a/probables/hashes.py b/probables/hashes.py index b7006b9..5b440e7 100644 --- a/probables/hashes.py +++ b/probables/hashes.py @@ -1,13 +1,13 @@ """Probables Hashing Utilities""" +from collections.abc import Callable from functools import wraps from hashlib import md5, sha256 from struct import unpack -from typing import Callable, Union from probables.constants import UINT32_T_MAX, UINT64_T_MAX -KeyT = Union[str, bytes] +KeyT = str | bytes SimpleHashT = Callable[[KeyT, int], int] SimpleHashBytesT = Callable[[KeyT, int], bytes] HashResultsT = list[int] @@ -15,7 +15,7 @@ HashFuncBytesT = Callable[[KeyT, int], bytes] -def hash_with_depth_bytes(func: Union[HashFuncBytesT, SimpleHashBytesT]) -> HashFuncT: +def hash_with_depth_bytes(func: HashFuncBytesT | SimpleHashBytesT) -> HashFuncT: """Decorator to turns a function taking a single key and hashes it to bytes. Wraps functions to be used in Bloom filters and Count-Min sketch data structures. @@ -41,7 +41,7 @@ def hashing_func(key, depth=1): return hashing_func -def hash_with_depth_int(func: Union[HashFuncT, SimpleHashT]) -> HashFuncT: +def hash_with_depth_int(func: HashFuncT | SimpleHashT) -> HashFuncT: """Decorator to turn a function that takes a single key and hashes it to an int. Wraps functions to be used in Bloom filters and Count-Min sketch data structures. diff --git a/probables/quotientfilter/quotientfilter.py b/probables/quotientfilter/quotientfilter.py index 7a09174..cad33d9 100644 --- a/probables/quotientfilter/quotientfilter.py +++ b/probables/quotientfilter/quotientfilter.py @@ -6,7 +6,7 @@ import sys from array import array from collections.abc import Iterator -from typing import Optional, TextIO +from typing import TextIO from probables.exceptions import QuotientFilterError from probables.hashes import KeyT, SimpleHashT, fnv_1a_32 @@ -45,7 +45,7 @@ class QuotientFilter: ) def __init__( - self, quotient: int = 20, auto_expand: bool = True, hash_function: Optional[SimpleHashT] = None + self, quotient: int = 20, auto_expand: bool = True, hash_function: SimpleHashT | None = None ): # needs to be parameterized if quotient < 3 or quotient > 31: raise QuotientFilterError( @@ -53,7 +53,7 @@ def __init__( ) self.__set_params(quotient, auto_expand, hash_function) - def __set_params(self, quotient: int, auto_expand: bool, hash_function: Optional[SimpleHashT]): + def __set_params(self, quotient: int, auto_expand: bool, hash_function: SimpleHashT | None): self._q: int = quotient self._r: int = 32 - quotient self._size: int = 1 << self._q # same as 2**q @@ -244,7 +244,7 @@ def get_hashes(self) -> list[int]: list(int): The hash values stored in the quotient filter""" return list(self.hashes()) - def resize(self, quotient: Optional[int] = None) -> None: + def resize(self, quotient: int | None = None) -> None: """Resize the quotient filter to use the new quotient size Args: diff --git a/probables/utilities.py b/probables/utilities.py index fa6c2df..9a6a6bb 100644 --- a/probables/utilities.py +++ b/probables/utilities.py @@ -7,24 +7,24 @@ from io import IOBase from pathlib import Path from struct import Struct -from typing import Literal, Union +from typing import Literal -def is_hex_string(hex_string: Union[str, None]) -> bool: +def is_hex_string(hex_string: str | None) -> bool: """check if the passed in string is really hex""" if hex_string is None: return False return all(c in string.hexdigits for c in hex_string) -def is_valid_file(filepath: Union[str, Path, None]) -> bool: +def is_valid_file(filepath: str | Path | None) -> bool: """check if the passed filepath points to a real file""" if filepath is None: return False return Path(filepath).exists() -def resolve_path(filepath: Union[str, Path]) -> Path: +def resolve_path(filepath: str | Path) -> Path: """fully resolve the path by expanding user and resolving""" return Path(filepath).expanduser().resolve() @@ -41,7 +41,7 @@ class MMap: __slots__ = ("__p", "__f", "__m", "_closed") - def __init__(self, path: Union[Path, str]): + def __init__(self, path: Path | str): self.__p = Path(path) self.__f = self.path.open("rb") # noqa: SIM115 self.__m = mmap.mmap(self.__f.fileno(), 0, access=mmap.ACCESS_READ) @@ -216,12 +216,12 @@ def from_bytes(cls, data: bytes) -> "Bitarray": ba._bitarray = bitarray return ba - def export(self, file: Union[Path, str, IOBase, mmap.mmap]) -> None: + def export(self, file: Path | str | IOBase | mmap.mmap) -> None: """Export the bitarray to a file Args: filename (str): Filename to export to""" - if not isinstance(file, (IOBase, mmap.mmap)): + if not isinstance(file, IOBase | mmap.mmap): file = resolve_path(file) with open(file, "wb") as filepointer: self.export(filepointer) diff --git a/pyproject.toml b/pyproject.toml index 4f43705..3cdf02f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,14 +27,13 @@ classifiers = [ "Topic :: Utilities", "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", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -requires-python = ">=3.9" +requires-python = ">=3.10" [tool.setuptools.dynamic] version = { attr = "probables.__version__" } @@ -65,7 +64,7 @@ profile = "black" [tool.black] line-length = 120 -target-version = ['py39'] +target-version = ['py310'] include = '\.pyi?$' [tool.ruff] @@ -102,7 +101,7 @@ exclude = [ # Same as Black. line-length = 120 indent-width = 4 -target-version = "py39" +target-version = "py310" [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. diff --git a/tests/cuckoo_test.py b/tests/cuckoo_test.py index 969f92b..1ee34e0 100755 --- a/tests/cuckoo_test.py +++ b/tests/cuckoo_test.py @@ -7,7 +7,6 @@ import unittest from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Union this_dir = Path(__file__).parent sys.path.insert(0, str(this_dir)) @@ -63,7 +62,7 @@ def test_cuckoo_filter_add(self): def test_cuckoo_filter_diff_hash(self): """test using a different hash function""" - def my_hash(key: Union[str, bytes], depth: int = 1) -> int: + def my_hash(key: str | bytes, depth: int = 1) -> int: """fake hash""" k = key if isinstance(key, bytes) else key.encode("utf-8") return int(hashlib.sha512(k).hexdigest(), 16) diff --git a/tests/utilities.py b/tests/utilities.py index 9d0e875..7c2f3ab 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -2,13 +2,12 @@ from hashlib import md5 from pathlib import Path -from typing import Union from probables.constants import UINT64_T_MAX from probables.hashes import KeyT -def calc_file_md5(filename: Union[str, Path]) -> str: +def calc_file_md5(filename: str | Path) -> str: """calc the md5 of a file""" with open(filename, "rb") as filepointer: res = filepointer.read() From 2a1b6ba7148da094663cc0319d6a8e630d8a9eb7 Mon Sep 17 00:00:00 2001 From: barrust Date: Thu, 29 Jan 2026 20:34:38 -0500 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 1 + README.rst | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae09fa6..ad8a9d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Minor breaking changes; mismatched Bloom filters raise a `SimilarityError` inste * Add ability to read and write as bytes * Add abilitt to export * Updated typing to be more consistent and correct +* Drop python 3.9 support ### Version 0.6.2 diff --git a/README.rst b/README.rst index e9d84f8..30699ad 100644 --- a/README.rst +++ b/README.rst @@ -61,13 +61,13 @@ To install `pyprobables`, simply clone the `repository on GitHub $ python setup.py install -`pyprobables` supports python 3.6 - 3.11+ +`pyprobables` supports python 3.10 - 3.14+ For *python 2.7* support, install `release 0.3.2 `__ :: - $ pip install pyprobables==0.3.2 + $ pip install pyprobables==0.7.0 API Documentation