diff --git a/.github/workflows/hamilton-lsp.yml b/.github/workflows/hamilton-lsp.yml index 83e17dc67..b1d23c224 100644 --- a/.github/workflows/hamilton-lsp.yml +++ b/.github/workflows/hamilton-lsp.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12', '3.13'] defaults: run: working-directory: dev_tools/language_server diff --git a/.github/workflows/hamilton-main.yml b/.github/workflows/hamilton-main.yml index 15495039a..b1039c81c 100644 --- a/.github/workflows/hamilton-main.yml +++ b/.github/workflows/hamilton-main.yml @@ -23,11 +23,10 @@ jobs: os: - ubuntu-latest python-version: - - '3.8' - - '3.9' - '3.10' - '3.11' - '3.12' + - '3.13' env: UV_PRERELEASE: "allow" HAMILTON_TELEMETRY_ENABLED: false diff --git a/.github/workflows/hamilton-sdk.yml b/.github/workflows/hamilton-sdk.yml index 5ec89e5aa..d5f539f41 100644 --- a/.github/workflows/hamilton-sdk.yml +++ b/.github/workflows/hamilton-sdk.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12', '3.13'] defaults: run: working-directory: ui/sdk diff --git a/hamilton/cli/__main__.py b/hamilton/cli/__main__.py index 41c411c6e..e5321bcb4 100644 --- a/hamilton/cli/__main__.py +++ b/hamilton/cli/__main__.py @@ -19,16 +19,10 @@ import json import logging import os -import sys import warnings from pathlib import Path from pprint import pprint -from typing import Any, Callable, List, Optional - -if sys.version_info < (3, 9): - from typing_extensions import Annotated -else: - from typing import Annotated +from typing import Annotated, Any, Callable, List, Optional import typer diff --git a/hamilton/htypes.py b/hamilton/htypes.py index 6849ed56a..ad374243a 100644 --- a/hamilton/htypes.py +++ b/hamilton/htypes.py @@ -18,14 +18,10 @@ import inspect import sys import typing -from typing import Any, Iterable, Optional, Protocol, Tuple, Type, TypeVar, Union +from typing import Any, Iterable, Literal, Optional, Protocol, Tuple, Type, TypeVar, Union import typing_inspect -if sys.version_info >= (3, 9): - from typing import Literal -else: - Literal = None from hamilton.registry import COLUMN_TYPE, DF_TYPE_AND_COLUMN_TYPES BASE_ARGS_FOR_GENERICS = (typing.T,) diff --git a/hamilton/node.py b/hamilton/node.py index 5a0ba3f56..36ffd6f21 100644 --- a/hamilton/node.py +++ b/hamilton/node.py @@ -118,8 +118,9 @@ def __init__( # assume optional values passed self._default_parameter_values = optional_values if optional_values else {} else: - # TODO -- remove this when we no longer support 3.8 -- 10/14/2024 - type_hint_kwargs = {} if sys.version_info < (3, 9) else {"include_extras": True} + type_hint_kwargs: dict[str, Any] = {"include_extras": True} + if sys.version_info >= (3, 13): + type_hint_kwargs["globalns"] = callabl.__globals__ input_types = typing.get_type_hints(callabl, **type_hint_kwargs) signature = inspect.signature(callabl) for key, value in signature.parameters.items(): @@ -291,8 +292,7 @@ def from_fn(fn: Callable, name: str = None) -> "Node": """ if name is None: name = fn.__name__ - # TODO -- remove this when we no longer support 3.8 -- 10/14/2024 - type_hint_kwargs = {} if sys.version_info < (3, 9) else {"include_extras": True} + type_hint_kwargs = {"include_extras": True} return_type = typing.get_type_hints(fn, **type_hint_kwargs).get("return") if return_type is None: raise ValueError(f"Missing type hint for return value in function {fn.__qualname__}.") diff --git a/hamilton/plugins/h_spark.py b/hamilton/plugins/h_spark.py index 3d0f7e33a..ae494319e 100644 --- a/hamilton/plugins/h_spark.py +++ b/hamilton/plugins/h_spark.py @@ -18,7 +18,6 @@ import functools import inspect import logging -import sys from types import CodeType, FunctionType, ModuleType from typing import Any, Callable, Collection, Dict, List, Optional, Set, Tuple, Type, Union @@ -229,10 +228,7 @@ def python_to_spark_type(python_type: Type[Union[int, float, bool, str, bytes]]) raise ValueError("Unsupported Python type: " + str(python_type)) -if sys.version_info < (3, 9): - _list = (List[int], List[float], List[bool], List[str], List[bytes]) -else: - _list = (list[int], list[float], list[bool], list[str], list[bytes]) +_list = (list[int], list[float], list[bool], list[str], list[bytes]) def get_spark_type(return_type: Any) -> types.DataType: diff --git a/plugin_tests/h_spark/test_h_spark.py b/plugin_tests/h_spark/test_h_spark.py index 0fcb44567..8a36d5ab4 100644 --- a/plugin_tests/h_spark/test_h_spark.py +++ b/plugin_tests/h_spark/test_h_spark.py @@ -15,7 +15,6 @@ # specific language governing permissions and limitations # under the License. -import sys import numpy as np import pandas as pd @@ -356,7 +355,6 @@ def test_get_spark_type_basic_types(return_type, expected_spark_type): # 2. Lists of basic Python types -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 or higher") @pytest.mark.parametrize( "return_type,expected_spark_type", [ diff --git a/pyproject.toml b/pyproject.toml index 2e58e897e..bfa3f85e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ version = "1.89.0" # NOTE: keep this in sync with hamilton/version.py # dynamic = ["version"] description = "Hamilton, the micro-framework for creating dataframes." readme = "README.md" -requires-python = ">=3.8.1, <4" +requires-python = ">=3.10.1, <4" license = {text = "Apache-2.0"} keywords = ["hamilton"] authors = [ @@ -39,11 +39,10 @@ classifiers = [ "Natural Language :: English", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13" ] dependencies = [ "numpy", diff --git a/tests/function_modifiers/test_adapters.py b/tests/function_modifiers/test_adapters.py index e95311a41..56052d5b5 100644 --- a/tests/function_modifiers/test_adapters.py +++ b/tests/function_modifiers/test_adapters.py @@ -711,14 +711,8 @@ def fn(data1: dict, data2: dict) -> dict: assert len(fg) == 5 -import sys - -if sys.version_info >= (3, 9): - dict_ = dict - tuple_ = tuple -else: - dict_ = Dict - tuple_ = Tuple +dict_ = dict +tuple_ = tuple # Mock functions for dataloader & datasaver testing @@ -770,10 +764,6 @@ def test_dl_validate_incorrect_functions(func): dl.validate(func) -@pytest.mark.skipif( - sys.version_info < (3, 9, 0), - reason="dataloader not guarenteed to work with subscripted tuples on 3.8", -) def test_dl_validate_with_correct_function(): dl = dataloader() try: diff --git a/tests/function_modifiers/test_expanders.py b/tests/function_modifiers/test_expanders.py index ee9d1b17a..bbc2fef2c 100644 --- a/tests/function_modifiers/test_expanders.py +++ b/tests/function_modifiers/test_expanders.py @@ -38,10 +38,6 @@ # TODO: Move/refactor for more general use skipif = pytest.mark.skipif -prior_to_py39 = { - "condition": sys.version_info < (3, 9, 0), - "reason": "Python 3.9+ required for this test", -} prior_to_py311 = { "condition": sys.version_info < (3, 11, 0), "reason": "Python 3.11+ required for this test", @@ -377,9 +373,9 @@ class MyDictInheritanceBadCase(TypedDict): ("MyDict", ()), ("MyDict", {"test2": str}), ("MyDictInheritance", {"test": InheritedObject}), - pytest.param("dict[str, int]", ("A", "B"), marks=skipif(**prior_to_py39)), - pytest.param("dict[str, int]", (["A", "B"]), marks=skipif(**prior_to_py39)), - pytest.param("dict", {"A": str, "B": int}, marks=skipif(**prior_to_py39)), + ("dict[str, int]", ("A", "B")), + ("dict[str, int]", (["A", "B"])), + ("dict", {"A": str, "B": int}), ], ) def test_extract_fields_valid_annotations_for_inferred_types(return_type_str, fields): @@ -408,10 +404,10 @@ def function() -> return_type: # type: ignore ("pd.DataFrame", {"A": int}), ("MyDictBad", {"A": int}), ("MyDictInheritanceBadCase", {"A": SomeObject}), - pytest.param("dict", ("A", "B"), marks=skipif(**prior_to_py39)), - pytest.param("dict", (["A", "B"]), marks=skipif(**prior_to_py39)), - pytest.param("dict", (["A"]), marks=skipif(**prior_to_py39)), - pytest.param("dict", (["A", "B", "C"]), marks=skipif(**prior_to_py39)), + ("dict", ("A", "B")), + ("dict", (["A", "B"])), + ("dict", (["A"])), + ("dict", (["A", "B", "C"])), ], ) def test_extract_fields_invalid_annotations_for_inferred_types(return_type_str, fields): @@ -781,9 +777,9 @@ def dummy() -> Tuple[int, ...]: ("Tuple[int, int]", ("A", "B")), ("Tuple[int, int, str]", ("A", "B", "C")), ("Tuple[int, ...]", ("A", "B")), - pytest.param("tuple[int, int]", ("A", "B"), marks=skipif(**prior_to_py39)), - pytest.param("tuple[int, int, str]", ("A", "B", "C"), marks=skipif(**prior_to_py39)), - pytest.param("tuple[int, ...]", ("A", "B"), marks=skipif(**prior_to_py39)), + ("tuple[int, int]", ("A", "B")), + ("tuple[int, int, str]", ("A", "B", "C")), + ("tuple[int, ...]", ("A", "B")), ], ) def test_unpack_fields_valid_type_annotations(return_type_str, fields): @@ -807,11 +803,11 @@ def function() -> return_type: pytest.param("Tuple[...]", ("A", "B", "C"), marks=skipif(**prior_to_py311)), pytest.param("Tuple[int, int, ...]", ("A", "B"), marks=skipif(**prior_to_py311)), pytest.param("Tuple[..., int, int]", ("A", "B"), marks=skipif(**prior_to_py311)), - pytest.param("tuple", ("A",), marks=skipif(**prior_to_py39)), - pytest.param("tuple[int, int]", ("A", "B", "C"), marks=skipif(**prior_to_py39)), - pytest.param("tuple[...]", ("A", "B", "C"), marks=skipif(**prior_to_py39)), - pytest.param("tuple[int, int, ...]", ("A", "B"), marks=skipif(**prior_to_py39)), - pytest.param("tuple[..., int, int]", ("A", "B"), marks=skipif(**prior_to_py39)), + ("tuple", ("A",)), + ("tuple[int, int]", ("A", "B", "C")), + ("tuple[...]", ("A", "B", "C")), + ("tuple[int, int, ...]", ("A", "B")), + ("tuple[..., int, int]", ("A", "B")), ], ) def test_unpack_fields_invalid_type_annotations(return_type_str, fields): @@ -1112,9 +1108,7 @@ def foo(x: int) -> int: annotation.validate(foo) -@pytest.mark.skipif(**prior_to_py39) def test_inject_misconfigured_param_untyped_generic_list(): - # NOTE: Stricter typing rules for generics were introduced in Python 3.9. def foo(x: List) -> int: return sum(x) diff --git a/tests/integrations/pandera/test_pandera_data_quality.py b/tests/integrations/pandera/test_pandera_data_quality.py index ef346776a..ac1a91ed8 100644 --- a/tests/integrations/pandera/test_pandera_data_quality.py +++ b/tests/integrations/pandera/test_pandera_data_quality.py @@ -15,7 +15,6 @@ # specific language governing permissions and limitations # under the License. -import sys import dask.dataframe as dd import numpy as np @@ -165,7 +164,6 @@ def foo() -> pd.DataFrame: h_pandera.check_output().get_validators(n) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9 or higher") def test_pandera_decorator_dask_df(): """Validates that the function can be annotated with a dask dataframe type it'll work appropriately. @@ -216,7 +214,6 @@ def foo(fail: bool = False) -> dd.DataFrame: assert not result_success.passes -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9 or higher") @pytest.mark.xfail( reason="some weird import issue leads to key error in pandera, can't recreate outside of the series decorator" ) diff --git a/tests/plugins/test_dlt_extensions.py b/tests/plugins/test_dlt_extensions.py index fa899fc7c..96795aaef 100644 --- a/tests/plugins/test_dlt_extensions.py +++ b/tests/plugins/test_dlt_extensions.py @@ -15,24 +15,15 @@ # specific language governing permissions and limitations # under the License. -import sys from pathlib import Path -import pytest - -PY38_OR_BELOW = sys.version_info < (3, 9) -pytestmark = pytest.mark.skipif( - PY38_OR_BELOW, reason="Breaks for python 3.8 and below due to backports dependency." -) - -if not PY38_OR_BELOW: - import dlt - from dlt.destinations import filesystem - - from hamilton.plugins.dlt_extensions import DltDestinationSaver, DltResourceLoader - +import dlt import pandas as pd import pyarrow as pa +import pytest +from dlt.destinations import filesystem + +from hamilton.plugins.dlt_extensions import DltDestinationSaver, DltResourceLoader def pandas_df(): diff --git a/tests/plugins/test_huggingface_extensions.py b/tests/plugins/test_huggingface_extensions.py index 664ab1229..7b52446a1 100644 --- a/tests/plugins/test_huggingface_extensions.py +++ b/tests/plugins/test_huggingface_extensions.py @@ -16,11 +16,9 @@ # under the License. import pathlib -import sys import lancedb import numpy as np -import pytest from datasets import Dataset, DatasetDict from hamilton.plugins import huggingface_extensions @@ -59,7 +57,6 @@ def test_hfds_parquet_saver(tmp_path: pathlib.Path): assert saver.applies_to(Dataset) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") def test_hfds_lancedb_saver(tmp_path: pathlib.Path): db_client = lancedb.connect(tmp_path / "lancedb") saver = huggingface_extensions.HuggingFaceDSLanceDBSaver(db_client, "test_table") diff --git a/tests/plugins/test_mlflow_extension.py b/tests/plugins/test_mlflow_extension.py index 8b89212b8..d98d3ed47 100644 --- a/tests/plugins/test_mlflow_extension.py +++ b/tests/plugins/test_mlflow_extension.py @@ -15,27 +15,16 @@ # specific language governing permissions and limitations # under the License. -import sys from pathlib import Path -import pytest - -PY38_OR_BELOW = sys.version_info < (3, 9) -pytestmark = pytest.mark.skipif( - PY38_OR_BELOW, reason="Breaks for python 3.8 and below due to backports dependency." -) - -if not PY38_OR_BELOW: - import mlflow - - from hamilton.plugins.mlflow_extensions import MLFlowModelLoader, MLFlowModelSaver - +import mlflow import numpy as np import pytest from sklearn.base import BaseEstimator from sklearn.linear_model import LinearRegression from hamilton.io.materialization import from_, to +from hamilton.plugins.mlflow_extensions import MLFlowModelLoader, MLFlowModelSaver # TODO move these tests to `plugin_tests` because the required read-writes can get # complicated and tests are time consuming. diff --git a/tests/resources/nodes_with_future_annotation.py b/tests/resources/nodes_with_future_annotation.py index f74ede77f..c577e80d7 100644 --- a/tests/resources/nodes_with_future_annotation.py +++ b/tests/resources/nodes_with_future_annotation.py @@ -17,17 +17,11 @@ from __future__ import annotations -import sys -from typing import List, Tuple - from hamilton.function_modifiers import dataloader from hamilton.htypes import Collect, Parallelizable """Tests future annotations with common node types""" -tuple_ = Tuple if sys.version_info < (3, 9, 0) else tuple -list_ = List if sys.version_info < (3, 9, 0) else list - def parallelized() -> Parallelizable[int]: yield 1 @@ -44,6 +38,6 @@ def collected(standard: Collect[int]) -> int: @dataloader() -def sample_dataloader() -> tuple_[list_[str], dict]: +def sample_dataloader() -> tuple[list[str], dict]: """Grouping here as the rest test annotations""" return ["a", "b", "c"], {} diff --git a/tests/test_graph_types.py b/tests/test_graph_types.py index a152716f0..a289a2660 100644 --- a/tests/test_graph_types.py +++ b/tests/test_graph_types.py @@ -216,28 +216,24 @@ def test_json_serializable_dict(): json.dumps(hamilton_node.as_dict()) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") def test_remove_docstring(func_a: str, func_a_docstring: str): func_a_no_whitespace = func_a.strip() stripped = graph_types._remove_docs_and_comments(func_a_docstring) assert func_a_no_whitespace == stripped -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") def test_remove_multiline(func_a: str, func_a_multiline: str): func_a_no_whitespace = func_a.strip() stripped = graph_types._remove_docs_and_comments(func_a_multiline) assert func_a_no_whitespace == stripped -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") def test_remove_comment(func_a: str, func_a_comment: str): func_a_no_whitespace = func_a.strip() stripped = graph_types._remove_docs_and_comments(func_a_comment) assert func_a_no_whitespace == stripped -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") @pytest.mark.parametrize("strip", [True, False]) def test_different_hash_function_body(func_a: str, func_a_body: str, strip: bool): """Gives different hash for different function body""" @@ -246,7 +242,6 @@ def test_different_hash_function_body(func_a: str, func_a_body: str, strip: bool assert func_a_hash != func_a_body_hash -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") @pytest.mark.parametrize("strip", [True, False]) def test_different_hash_docstring(func_a: str, func_a_docstring: str, strip: bool): """Same hash if strip docstring, else different hash""" @@ -255,7 +250,6 @@ def test_different_hash_docstring(func_a: str, func_a_docstring: str, strip: boo assert (func_a_hash == func_a_docstring_hash) is (True if strip else False) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") @pytest.mark.parametrize("strip", [True, False]) def test_different_hash_multiline_docstring(func_a: str, func_a_multiline: str, strip: bool): """Same hash if strip multiline docstring, else different hash""" @@ -264,7 +258,6 @@ def test_different_hash_multiline_docstring(func_a: str, func_a_multiline: str, assert (func_a_hash == func_a_multiline_hash) is (True if strip else False) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or higher") @pytest.mark.parametrize("strip", [True, False]) def test_different_hash_comment(func_a: str, func_a_comment: str, strip: bool): """Same hash if strip comment, else different hash""" diff --git a/tests/test_node.py b/tests/test_node.py index 0411f0557..e77600ea7 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -65,7 +65,6 @@ def fn() -> int: major, minor, _ = map(int, np_version.split(".")) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 or higher") def test_node_handles_annotated(): from typing import Annotated diff --git a/tests/test_type_utils.py b/tests/test_type_utils.py index a49e47e1a..5ff1cb130 100644 --- a/tests/test_type_utils.py +++ b/tests/test_type_utils.py @@ -16,9 +16,8 @@ # under the License. import collections -import sys import typing -from typing import Any, Dict, List, Union +from typing import Annotated, Any, Dict, List, Union import pandas as pd import pytest @@ -26,11 +25,6 @@ from hamilton import htypes from hamilton.htypes import check_instance -if sys.version_info >= (3, 9): - from typing import Annotated -else: - from typing_extensions import Annotated - class X: pass @@ -308,24 +302,12 @@ def test_check_input_type_match(node_type, input_value): assert actual is True -# We cannot parameterize this as the parameterization cannot be -# included if the -@pytest.mark.skipif( - sys.version_info < (3, 9, 0), - reason="Type hinting generics in standard collections " "is only supported in 3.9+", -) def test_check_input_types_subscripted_generics_dict_str_Any(): """Tests check_input_type of SimplePythonDataFrameGraphAdapter""" actual = htypes.check_input_type(dict[str, typing.Any], {}) assert actual is True -# We cannot parameterize this as the parameterization cannot be -# included if the -@pytest.mark.skipif( - sys.version_info < (3, 9, 0), - reason="Type hinting generics in standard collections " "is only supported in 3.9+", -) def test_check_input_types_subscripted_generics_list_Any(): """Tests check_input_type of SimplePythonDataFrameGraphAdapter""" actual = htypes.check_input_type(list[typing.Any], []) @@ -340,10 +322,8 @@ def test_check_instance_with_non_generic_type(): def test_check_instance_with_generic_list_type(): assert check_instance([1, 2, 3], List[int]) assert not check_instance([1, 2, "3"], List[int]) - if sys.version_info >= (3, 9): - # skip 3.8 -- not worth fixing - assert check_instance([1, 2, 3], List) - assert check_instance([1, 2, "3"], List) + assert check_instance([1, 2, 3], List) + assert check_instance([1, 2, "3"], List) def test_check_instance_with_list_type(): @@ -354,10 +334,8 @@ def test_check_instance_with_list_type(): def test_check_instance_with_generic_dict_type(): assert check_instance({"key1": 1, "key2": 2}, Dict[str, int]) assert not check_instance({"key1": 1, "key2": "2"}, Dict[str, int]) - if sys.version_info >= (3, 9): - # skip 3.8 -- not worth fixing - assert check_instance({"key1": 1, "key2": 2}, Dict) - assert check_instance({"key1": 1, "key2": "2"}, Dict) + assert check_instance({"key1": 1, "key2": 2}, Dict) + assert check_instance({"key1": 1, "key2": "2"}, Dict) def test_check_instance_with_dict_type(): @@ -391,7 +369,6 @@ def test_check_instance_with_union_type(): assert not check_instance({"key1": 1, "key2": 2}, Union[int, str]) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 or higher") def test_check_instance_with_union_type_and_literal(): from typing import Literal @@ -400,7 +377,6 @@ def test_check_instance_with_union_type_and_literal(): assert not check_instance("c", Union[Literal["a"], Literal["b"]]) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 or higher") def test_non_generic_dict_and_list(): assert check_instance([1, 2, 3], list[int]) assert not check_instance([1, 2, "3"], list[int])