diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b44123d5..69e18a2d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v5 @@ -63,7 +63,7 @@ jobs: uses: codecov/codecov-action@v5 - name: Run tests with lowest resolution - if: matrix.python-version == '3.9' && matrix.os == 'ubuntu-latest' + if: matrix.python-version == '3.10' && matrix.os == 'ubuntu-latest' run: just test-lowest - name: Run tests with highest resolution diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c610339..8b39513a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - {pull}`703` fixes {issue}`701` by allowing `--capture tee-sys` again. - {pull}`704` adds the `--explain` flag to show why tasks would be executed. Closes {issue}`466`. - {pull}`706` disables syntax highlighting for platform version information in session header. +- {pull}`707` drops support for Python 3.9 as it has reached end of life. ## 0.5.5 - 2025-07-25 diff --git a/docs/source/_static/md/pdb.md b/docs/source/_static/md/pdb.md index c510ce7b..3017f26e 100644 --- a/docs/source/_static/md/pdb.md +++ b/docs/source/_static/md/pdb.md @@ -8,7 +8,7 @@ Platform: win32 -- Python 3.12.0, pytask 0.5.3, pluggy 1.3.0 Root: C:\Users\pytask-dev\git\my_project Collected 1 task. ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ╭─────────────────── Traceback (most recent call last) ─────────────────╮ .../task_data_preparation.py:23 in task_create_random_data @@ -24,7 +24,7 @@ Collected 1 task. Exception >>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>> -> ...\git\my_project\task_data_preparation.py(23)task_create_random_data() +> ...\my_project\task_data_preparation.py(23)task_create_random_data() -> raise Exception diff --git a/docs/source/_static/md/show-locals.md b/docs/source/_static/md/show-locals.md index 8152e69c..6e6c3655 100644 --- a/docs/source/_static/md/show-locals.md +++ b/docs/source/_static/md/show-locals.md @@ -3,7 +3,7 @@ ```console $ pytask --show-locals -──────────────────────────── Start pytask session ──────────────────────────── +────────────────────────── Start pytask session ───────────────────────── Platform: win32 -- Python 3.12.0, pytask 0.5.3, pluggy 1.3.0 Root: C:\Users\pytask-dev\git\my_project Collected 1 task. @@ -14,45 +14,45 @@ Collected 1 task. │ task_create_random_data.py::task_create_random_data │ F │ └─────────────────────────────────────────────────────┴─────────┘ -────────────────────────────────── Failures ────────────────────────────────── - -─────── Task task_create_random_data.py::task_create_random_data failed ────── - -╭───────────────────── Traceback (most recent call last) ────────────────────╮ - - .../task_data_preparation.py:23 in task_create_random_data - - 20 │ - 21 │ df = pd.DataFrame({"x": x, "y": y}) - 22 │ - 23 raise Exception - 24 │ - 25 │ df.to_pickle(produces) - 26 - - ╭──────────────────────────────── locals ─────────────────────────────────╮ - beta = 2 - df = x y - 0 6.257302 12.876199 - 1 3.678951 8.661903 - 2 11.404227 23.755534 - 3 6.049001 11.394267 - 4 -0.356694 -1.978809 - epsilon = array([ 0.36159, 1.30400, 0.94708, -0.70373, -1.26542]) - produces = WindowsPath('C:/Users/pytask-dev/git/my_project/data.pkl') - rng = Generator(PCG64) at 0x20987EC6340 - x = array([ 6.25730, 3.67895, 11.40422 , 6.04900, -0.35669]) - y = array([12.87619, 8.66190, 23.75553, 11.39426, -1.97880]) - ╰─────────────────────────────────────────────────────────────────────────╯ -╰────────────────────────────────────────────────────────────────────────────╯ +──────────────────────────────── Failures ─────────────────────────────── + +──── Task task_create_random_data.py::task_create_random_data failed ──── + +╭────────────────── Traceback (most recent call last) ──────────────────╮ + + .../task_data_preparation.py:23 in task_create_random_data + + 20 │ + 21 │ df = pd.DataFrame({"x": x, "y": y}) + 22 │ + 23 │ raise Exception + 24 │ + 25 │ df.to_pickle(produces) + 26 + + ╭────────────────────────────── locals ──────────────────────────────╮ + beta = 2 + df = x y + 0 6.257302 12.876199 + 1 3.678951 8.661903 + 2 11.404227 23.755534 + 3 6.049001 11.394267 + 4 -0.356694 -1.978809 + epsilon = array([ 0.3615, 1.3040, 0.9471, -0.7037, -1.2654]) + produces = WindowsPath('.../git/my_project/data.pkl') + rng = Generator(PCG64) at 0x20987EC6340 + x = array([ 6.2573, 3.6789, 11.4042 , 6.0490, -0.3567]) + y = array([12.8762, 8.6619, 23.7555, 11.3943, -1.9788]) + ╰────────────────────────────────────────────────────────────────────╯ +╰───────────────────────────────────────────────────────────────────────╯ Exception -────────────────────────────────────────────────────────────────────────────── +───────────────────────────────────────────────────────────────────────── ╭─────────── Summary ────────────╮ 1 Collected tasks 1 Failed (100.0%) ╰────────────────────────────────╯ -─────────────────────────── Failed in 0.01 seconds ─────────────────────────── +───────────────────────── Failed in 0.01 seconds ──────────────────────── ``` diff --git a/docs/source/_static/md/trace.md b/docs/source/_static/md/trace.md index fc1dda5b..6626b4d9 100644 --- a/docs/source/_static/md/trace.md +++ b/docs/source/_static/md/trace.md @@ -3,14 +3,14 @@ ```console $ pytask --trace -──────────────────────────── Start pytask session ──────────────────────────── +────────────────────────── Start pytask session ───────────────────────── Platform: win32 -- Python 3.12.0, pytask 0.5.3, pluggy 1.3.0 Root: C:\Users\pytask-dev\git\my_project Collected 1 task. ->>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>> +>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>> -> ...\git\my_project\task_create_random_data.py(13)task_create_random_data() +> ...\my_project\task_create_random_data.py(13)task_create_random_data() -> rng = np.random.default_rng(0) diff --git a/docs/source/how_to_guides/capture_warnings.md b/docs/source/how_to_guides/capture_warnings.md index 627d59b5..b89072a3 100644 --- a/docs/source/how_to_guides/capture_warnings.md +++ b/docs/source/how_to_guides/capture_warnings.md @@ -87,13 +87,5 @@ and then run `pytask`. Or, you use a temporary environment variable. Here is an example for bash. ```console -PYTHONWARNINGS=error pytask --pdb -``` - -and here for Powershell - -```console -$env:PYTHONWARNINGS = 'error' -pytask -Remove-Item env:\PYTHONWARNINGS +$ PYTHONWARNINGS=error pytask --pdb ``` diff --git a/docs/source/how_to_guides/hashing_inputs_of_tasks.md b/docs/source/how_to_guides/hashing_inputs_of_tasks.md index d589d079..afd3144b 100644 --- a/docs/source/how_to_guides/hashing_inputs_of_tasks.md +++ b/docs/source/how_to_guides/hashing_inputs_of_tasks.md @@ -11,16 +11,9 @@ If an input is not parsed by any more specific node type, the general In the following example, the argument `text` will be parsed as a {class}`~pytask.PythonNode`. -`````{tab-set} - -````{tab-item} Python 3.10+ - ```{literalinclude} ../../../docs_src/how_to_guides/hashing_inputs_of_tasks_example_1_py310.py ``` -```` -````` - By default, pytask does not detect changes in {class}`~pytask.PythonNode` and if the value would change (without changing the task module), pytask would not rerun the task. @@ -28,16 +21,9 @@ We can also hash the value of {class}`~pytask.PythonNode` s so that pytask knows the input changed. For that, we need to use the {class}`~pytask.PythonNode` explicitly and set `hash = True`. -`````{tab-set} - -````{tab-item} Python 3.10+ - ```{literalinclude} ../../../docs_src/how_to_guides/hashing_inputs_of_tasks_example_2_py310.py ``` -```` -````` - When `hash=True`, pytask will call the builtin {func}`hash` on the input that will call the `__hash__()` method of the object. @@ -75,12 +61,5 @@ $ conda install deepdiff Then, create the hash function and pass it to the node. Make sure it returns either an integer or a string. -`````{tab-set} - -````{tab-item} Python 3.10+ - ```{literalinclude} ../../../docs_src/how_to_guides/hashing_inputs_of_tasks_example_3_py310.py ``` - -```` -````` diff --git a/docs/source/how_to_guides/provisional_nodes_and_task_generators.md b/docs/source/how_to_guides/provisional_nodes_and_task_generators.md index 3bef27f3..d43c9838 100644 --- a/docs/source/how_to_guides/provisional_nodes_and_task_generators.md +++ b/docs/source/how_to_guides/provisional_nodes_and_task_generators.md @@ -25,7 +25,7 @@ as the task module because it is a relative path. ```{literalinclude} ../../../docs_src/how_to_guides/provisional_products.py --- -emphasize-lines: 4, 22 +emphasize-lines: 6, 23 --- ``` @@ -57,7 +57,7 @@ downloaded. ```{literalinclude} ../../../docs_src/how_to_guides/provisional_task.py --- -emphasize-lines: 9 +emphasize-lines: 4, 9 --- ``` @@ -86,6 +86,9 @@ The code snippet shows each task takes one of the downloaded files and copies it content to a `.txt` file. ```{literalinclude} ../../../docs_src/how_to_guides/provisional_task_generator.py +--- +emphasize-lines: 4, 11 +--- ``` ```{important} diff --git a/docs/source/how_to_guides/using_task_returns.md b/docs/source/how_to_guides/using_task_returns.md index 737a4bdf..1e33def4 100644 --- a/docs/source/how_to_guides/using_task_returns.md +++ b/docs/source/how_to_guides/using_task_returns.md @@ -18,21 +18,14 @@ defines where the return of the function, a string, should be stored. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_1_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_1_py38.py -``` - -```` ````` It works because internally the path is converted to a {class}`pytask.PathNode` that is @@ -60,22 +53,14 @@ of the previous interfaces. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_3_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_3_py38.py -``` - -```` - ````{tab-item} @pytask.task ```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_3_task.py @@ -95,22 +80,14 @@ mapped to the defined nodes. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_4_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_4_py38.py -``` - -```` - ````{tab-item} @pytask.task ```{literalinclude} ../../../docs_src/how_to_guides/using_task_returns_example_4_task.py diff --git a/docs/source/how_to_guides/writing_custom_nodes.md b/docs/source/how_to_guides/writing_custom_nodes.md index 68bf0f6a..2810b0a3 100644 --- a/docs/source/how_to_guides/writing_custom_nodes.md +++ b/docs/source/how_to_guides/writing_custom_nodes.md @@ -26,37 +26,22 @@ The result will be the following task. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_2_py310.py ``` ```` -````{tab-item} Python 3.10+ & Return -:sync: python310plus +````{tab-item} Annotated & Return +:sync: annotated ```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_2_py310_return.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_2_py38.py -``` - -```` - -````{tab-item} Python 3.9 & Return -:sync: python38plus - -```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_2_py38_return.py -``` - -```` ````` ## Nodes @@ -79,21 +64,14 @@ we arrive at the following class. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_3_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_3_py38.py -``` - -```` ````` Here are some explanations. diff --git a/docs/source/tutorials/defining_dependencies_products.md b/docs/source/tutorials/defining_dependencies_products.md index 88e28c68..c92a1cf6 100644 --- a/docs/source/tutorials/defining_dependencies_products.md +++ b/docs/source/tutorials/defining_dependencies_products.md @@ -4,9 +4,8 @@ Define task dependencies and products to run your tasks. Defining dependencies and products also determines task execution order. -This tutorial offers you different interfaces. For type annotations, see the -`Python 3.10+` or `3.9` tabs. You find a tutorial on type hints -{doc}`here <../type_hints>`. +This tutorial offers you different interfaces. For type annotations, see the `Annotated` +tabs. You find a tutorial on type hints {doc}`here <../type_hints>`. If you want to avoid type annotations for now, look at the tab named `produces`. @@ -47,23 +46,11 @@ in `task_data_preparation.py`. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_products_py310.py -:emphasize-lines: 11 -``` - -{class}`~pytask.Product` allows marking an argument as a product. After the -task has finished, pytask will check whether the file exists. - -```` - -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_products_py38.py -:emphasize-lines: 11 +:emphasize-lines: 8, 12 ``` {class}`~pytask.Product` allows marking an argument as a product. After the @@ -101,8 +88,8 @@ we will define it in `task_plot_data.py`. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated To specify the task relies on `data.pkl`, add the path to the function signature with any argument name (here `path_to_data`). @@ -111,22 +98,7 @@ pytask assumes that all function arguments that do not have a {class}`~pytask.Pr annotation are dependencies of the task. ```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_dependencies_py310.py -:emphasize-lines: 11 -``` - -```` - -````{tab-item} Python 3.9 -:sync: python38plus - -To specify the task relies on `data.pkl`, add the path -to the function signature with any argument name (here `path_to_data`). - -pytask assumes that all function arguments that do not have the {class}`~pytask.Product` -annotation are dependencies of the task. - -```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_dependencies_py38.py -:emphasize-lines: 11 +:emphasize-lines: 12 ``` ```` @@ -159,8 +131,8 @@ are assumed to point to a location relative to the task module. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_relative_py310.py :emphasize-lines: 8 @@ -168,15 +140,6 @@ are assumed to point to a location relative to the task module. ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_relative_py38.py -:emphasize-lines: 8 -``` - -```` - ````{tab-item} produces :sync: produces @@ -193,8 +156,8 @@ Of course, tasks can have multiple dependencies and products. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_multiple1_py310.py ``` @@ -208,21 +171,6 @@ structures if needed. ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_multiple1_py38.py -``` - -You can group your dependencies and product if you prefer not to have a function -argument per input. Use dictionaries (recommended), tuples, lists, or more nested -structures if needed. - -```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_multiple2_py38.py -``` - -```` - ````{tab-item} produces :sync: produces diff --git a/docs/source/tutorials/repeating_tasks_with_different_inputs.md b/docs/source/tutorials/repeating_tasks_with_different_inputs.md index bd7c9e5c..41969251 100644 --- a/docs/source/tutorials/repeating_tasks_with_different_inputs.md +++ b/docs/source/tutorials/repeating_tasks_with_different_inputs.md @@ -13,22 +13,14 @@ different seeds and output paths as default arguments of the function. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs1_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs1_py38.py -``` - -```` - ````{tab-item} produces :sync: produces @@ -49,22 +41,14 @@ You can also add dependencies to repeated tasks just like with any other task. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs2_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs2_py38.py -``` - -```` - ````{tab-item} produces :sync: produces @@ -109,22 +93,14 @@ For example, the following function is parametrized with tuples. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs3_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs3_py38.py -``` - -```` - ````{tab-item} produces :sync: produces @@ -150,22 +126,14 @@ a unique name for the iteration. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs4_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs4_py38.py -``` - -```` - ````{tab-item} produces :sync: produces @@ -236,22 +204,14 @@ Following these three tips, the parametrization becomes `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs5_py310.py ``` ```` -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/repeating_tasks_with_different_inputs5_py38.py -``` - -```` - ````{tab-item} produces :sync: produces diff --git a/docs/source/tutorials/selecting_tasks.md b/docs/source/tutorials/selecting_tasks.md index 23b06903..a815e6c1 100644 --- a/docs/source/tutorials/selecting_tasks.md +++ b/docs/source/tutorials/selecting_tasks.md @@ -70,13 +70,12 @@ This command only runs the first two tasks. $ pytask -k "1 or 2 and not 12" ``` -To execute a single task, say `task_run_this_one` in `task_example.py`, use +To execute a single task, say `task_run_this_one` in `task_example.py`, use one of the +following commands. ```console $ pytask -k task_example.py::task_run_this_one -# or - $ pytask -k task_run_this_one ``` diff --git a/docs/source/tutorials/set_up_a_project.md b/docs/source/tutorials/set_up_a_project.md index 8dcb86d9..891e8d9b 100644 --- a/docs/source/tutorials/set_up_a_project.md +++ b/docs/source/tutorials/set_up_a_project.md @@ -86,7 +86,7 @@ Create a `pyproject.toml` file for project configuration and dependencies: [project] name = "my_project" version = "0.1.0" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = ["pytask"] [build-system] @@ -110,7 +110,7 @@ Create a `pixi.toml` file for project configuration: [project] name = "my_project" version = "0.1.0" -requires-python = ">=3.9" +requires-python = ">=3.10" channels = ["conda-forge"] platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"] @@ -120,7 +120,7 @@ build-backend = "hatchling.build" [dependencies] pytask = "*" -python = ">=3.9" +python = ">=3.10" [tool.pytask.ini_options] paths = ["src/my_project"] @@ -137,7 +137,7 @@ Create a `pyproject.toml` file for project configuration: [project] name = "my_project" version = "0.1.0" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = ["pytask"] [build-system] @@ -166,7 +166,7 @@ name: my_project channels: - conda-forge dependencies: - - python>=3.9 + - python>=3.10 - pytask - pip - pip: @@ -179,7 +179,7 @@ And a `pyproject.toml` file for project configuration: [project] name = "my_project" version = "0.1.0" -requires-python = ">=3.9" +requires-python = ">=3.10" [build-system] requires = ["hatchling"] diff --git a/docs/source/tutorials/using_a_data_catalog.md b/docs/source/tutorials/using_a_data_catalog.md index 66b4e173..292740ce 100644 --- a/docs/source/tutorials/using_a_data_catalog.md +++ b/docs/source/tutorials/using_a_data_catalog.md @@ -55,19 +55,11 @@ of our tasks. Here we see again the signature of the task function. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_products_py310.py -:lines: 10-12 -``` -```` - -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/defining_dependencies_products_products_py38.py -:lines: 10-12 +:lines: 11-12 ``` ```` @@ -94,8 +86,8 @@ The following tabs show you how to use the data catalog given the interface you `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated Use `data_catalog["data"]` as an default argument to access the {class}`~pytask.PickleNode` within the task. When you are done transforming your @@ -107,19 +99,6 @@ Use `data_catalog["data"]` as an default argument to access the ```` -````{tab-item} Python 3.9 -:sync: python38plus - -Use `data_catalog["data"]` as an default argument to access the -{class}`~pytask.PickleNode` within the task. When you are done transforming your -{class}`~pandas.DataFrame`, save it with {meth}`~pytask.PickleNode.save`. - -```{literalinclude} ../../../docs_src/tutorials/using_a_data_catalog_2_py38.py -:emphasize-lines: 10, 21 -``` - -```` - ````{tab-item} ​produces :sync: produces @@ -133,7 +112,7 @@ Use `data_catalog["data"]` as an default argument to access the ```` -````{tab-item} ​Python 3.10+ & Return +````{tab-item} ​Annotated & Return :sync: return An elegant way to use the data catalog is via return type annotations. Add @@ -158,20 +137,11 @@ Following one of the interfaces gives you immediate access to the `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/using_a_data_catalog_3_py310.py -:emphasize-lines: 12 -``` - -```` - -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/using_a_data_catalog_3_py38.py -:emphasize-lines: 12 +:emphasize-lines: 13 ``` ```` @@ -232,25 +202,16 @@ different node types which is not relevant now. `````{tab-set} -````{tab-item} Python 3.10+ -:sync: python310plus +````{tab-item} Annotated +:sync: annotated ```{literalinclude} ../../../docs_src/tutorials/using_a_data_catalog_5_py310.py -:emphasize-lines: 11, 12 -``` - -```` - -````{tab-item} Python 3.9 -:sync: python38plus - -```{literalinclude} ../../../docs_src/tutorials/using_a_data_catalog_5_py38.py -:emphasize-lines: 11, 12 +:emphasize-lines: 12, 13 ``` ```` -````{tab-item} ​Python 3.10+ & Return +````{tab-item} ​Annotated & Return :sync: return ```{literalinclude} ../../../docs_src/tutorials/using_a_data_catalog_5_py310_return.py diff --git a/docs/source/tutorials/write_a_task.md b/docs/source/tutorials/write_a_task.md index 214a8dd0..6dc17f82 100644 --- a/docs/source/tutorials/write_a_task.md +++ b/docs/source/tutorials/write_a_task.md @@ -40,7 +40,7 @@ You cannot mix different interfaces for the same task. Choose only one. `````{tab-set} -````{tab-item} Python 3.10+ +````{tab-item} Annotated The task accepts the argument `path` that points to the file where the data set will be stored. The path is passed to the task via the default value, `BLD / "data.pkl"`. To @@ -52,29 +52,7 @@ The type hint `Annotated[Path, Product]` uses argument as a product. ```{literalinclude} ../../../docs_src/tutorials/write_a_task_py310.py -:emphasize-lines: 3, 11 -``` - -```{tip} -If you want to refresh your knowledge about type hints, read -[this guide](../type_hints.md). -``` - -```` - -````{tab-item} Python 3.9 - -The task accepts the argument `path` that points to the file where the data set will be -stored. The path is passed to the task via the default value, `BLD / "data.pkl"`. To -indicate that this file is a product we add some metadata to the argument. - -The type hint `Annotated[Path, Product]` uses -{obj}`~typing.Annotated` syntax. The first entry specifies the argument type -({class}`~pathlib.Path`), and the second entry ({class}`~pytask.Product`) marks this -argument as a product. - -```{literalinclude} ../../../docs_src/tutorials/write_a_task_py38.py -:emphasize-lines: 8, 11 +:emphasize-lines: 3, 12 ``` ```{tip} diff --git a/docs_src/how_to_guides/migrating_from_scripts_to_pytask_5.py b/docs_src/how_to_guides/migrating_from_scripts_to_pytask_5.py index 17c5e99f..fafa99ee 100644 --- a/docs_src/how_to_guides/migrating_from_scripts_to_pytask_5.py +++ b/docs_src/how_to_guides/migrating_from_scripts_to_pytask_5.py @@ -1,6 +1,5 @@ from pathlib import Path from typing import Annotated -from typing import Optional import pandas as pd @@ -8,7 +7,7 @@ def task_merge_data( - paths_to_input_data: Optional[dict[str, Path]] = None, + paths_to_input_data: dict[str, Path] | None = None, path_to_merged_data: Annotated[Path, Product] = Path("merged_data.pkl"), ) -> None: if paths_to_input_data is None: diff --git a/docs_src/how_to_guides/using_task_returns_example_1_py38.py b/docs_src/how_to_guides/using_task_returns_example_1_py38.py deleted file mode 100644 index dabd1674..00000000 --- a/docs_src/how_to_guides/using_task_returns_example_1_py38.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path -from typing import Annotated - - -def task_create_file() -> Annotated[str, Path("file.txt")]: - return "This is the content of the text file." diff --git a/docs_src/how_to_guides/using_task_returns_example_3_py38.py b/docs_src/how_to_guides/using_task_returns_example_3_py38.py deleted file mode 100644 index a04086c0..00000000 --- a/docs_src/how_to_guides/using_task_returns_example_3_py38.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path -from typing import Annotated - - -def task_create_files() -> Annotated[str, (Path("file1.txt"), Path("file2.txt"))]: - return "This is the first content.", "This is the second content." diff --git a/docs_src/how_to_guides/using_task_returns_example_4_py38.py b/docs_src/how_to_guides/using_task_returns_example_4_py38.py deleted file mode 100644 index f4250cf1..00000000 --- a/docs_src/how_to_guides/using_task_returns_example_4_py38.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Annotated -from typing import Any - -from pytask import PythonNode - -nodes = [ - {"first": PythonNode(name="dict1"), "second": PythonNode(name="dict2")}, - (PythonNode(name="tuple1"), PythonNode(name="tuple2")), - PythonNode(name="int"), -] - - -def task_example() -> Annotated[Any, nodes]: - return [{"first": "a", "second": {"b": 1, "c": 2}}, (3, 4), 5] diff --git a/docs_src/how_to_guides/writing_custom_nodes_example_2_py38.py b/docs_src/how_to_guides/writing_custom_nodes_example_2_py38.py deleted file mode 100644 index cb56e273..00000000 --- a/docs_src/how_to_guides/writing_custom_nodes_example_2_py38.py +++ /dev/null @@ -1,20 +0,0 @@ -from pathlib import Path -from typing import Annotated - -import pandas as pd - -from pytask import Product - - -class PickleNode: ... - - -in_node = PickleNode.from_path(Path(__file__).parent / "in.pkl") -out_node = PickleNode.from_path(Path(__file__).parent / "out.pkl") - - -def task_example( - df: Annotated[pd.DataFrame, in_node], out: Annotated[PickleNode, out_node, Product] -) -> None: - transformed = df.apply(...) - out.save(transformed) diff --git a/docs_src/how_to_guides/writing_custom_nodes_example_2_py38_return.py b/docs_src/how_to_guides/writing_custom_nodes_example_2_py38_return.py deleted file mode 100644 index 33315cfa..00000000 --- a/docs_src/how_to_guides/writing_custom_nodes_example_2_py38_return.py +++ /dev/null @@ -1,17 +0,0 @@ -from pathlib import Path -from typing import Annotated - -import pandas as pd - - -class PickleNode: ... - - -in_node = PickleNode.from_path(Path(__file__).parent / "in.pkl") -out_node = PickleNode.from_path(Path(__file__).parent / "out.pkl") - - -def task_example( - df: Annotated[pd.DataFrame, in_node], -) -> Annotated[pd.DataFrame, out_node]: - return df.apply(...) diff --git a/docs_src/how_to_guides/writing_custom_nodes_example_3_py38.py b/docs_src/how_to_guides/writing_custom_nodes_example_3_py38.py deleted file mode 100644 index 583307d1..00000000 --- a/docs_src/how_to_guides/writing_custom_nodes_example_3_py38.py +++ /dev/null @@ -1,62 +0,0 @@ -import hashlib -import pickle -from pathlib import Path -from typing import Any -from typing import Optional - -from pytask import hash_value - - -class PickleNode: - """The class for a node that persists values with pickle to files. - - Parameters - ---------- - name - Name of the node which makes it identifiable in the DAG. - path - The path to the file. - attributes - Additional attributes that are stored in the node. - - """ - - def __init__( - self, - name: str = "", - path: Optional[Path] = None, - attributes: Optional[dict[Any, Any]] = None, - ) -> None: - self.name = name - self.path = path - self.attributes = attributes if attributes is not None else {} - - @property - def signature(self) -> str: - """The unique signature of the node.""" - raw_key = str(hash_value(self.path)) - return hashlib.sha256(raw_key.encode()).hexdigest() - - @classmethod - def from_path(cls, path: Path) -> "PickleNode": - """Instantiate class from path to file.""" - if not path.is_absolute(): - msg = "Node must be instantiated from absolute path." - raise ValueError(msg) - return cls(name=path.as_posix(), path=path) - - def state(self) -> Optional[str]: - """Return the modification timestamp as the state.""" - if self.path.exists(): - return str(self.path.stat().st_mtime) - return None - - def load(self, is_product: bool) -> Path: - """Load the value from the path.""" - if is_product: - return self - return pickle.loads(self.path.read_bytes()) - - def save(self, value: Any) -> None: - """Save any value with pickle to the file.""" - self.path.write_bytes(pickle.dumps(value)) diff --git a/docs_src/tutorials/defining_dependencies_products_dependencies_py38.py b/docs_src/tutorials/defining_dependencies_products_dependencies_py38.py deleted file mode 100644 index 01db3f75..00000000 --- a/docs_src/tutorials/defining_dependencies_products_dependencies_py38.py +++ /dev/null @@ -1,21 +0,0 @@ -from pathlib import Path -from typing import Annotated - -import matplotlib.pyplot as plt -import pandas as pd -from my_project.config import BLD - -from pytask import Product - - -def task_plot_data( - path_to_data: Path = BLD / "data.pkl", - path_to_plot: Annotated[Path, Product] = BLD / "plot.png", -) -> None: - df = pd.read_pickle(path_to_data) - - _, ax = plt.subplots() - df.plot(x="x", y="y", ax=ax, kind="scatter") - - plt.savefig(path_to_plot) - plt.close() diff --git a/docs_src/tutorials/defining_dependencies_products_multiple1_py38.py b/docs_src/tutorials/defining_dependencies_products_multiple1_py38.py deleted file mode 100644 index 34317089..00000000 --- a/docs_src/tutorials/defining_dependencies_products_multiple1_py38.py +++ /dev/null @@ -1,14 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from my_project.config import BLD - -from pytask import Product - - -def task_plot_data( - path_to_data_0: Path = BLD / "data_0.pkl", - path_to_data_1: Path = BLD / "data_1.pkl", - path_to_plot_0: Annotated[Path, Product] = BLD / "plot_0.png", - path_to_plot_1: Annotated[Path, Product] = BLD / "plot_1.png", -) -> None: ... diff --git a/docs_src/tutorials/defining_dependencies_products_multiple2_py38.py b/docs_src/tutorials/defining_dependencies_products_multiple2_py38.py deleted file mode 100644 index c5c703f9..00000000 --- a/docs_src/tutorials/defining_dependencies_products_multiple2_py38.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from my_project.config import BLD - -from pytask import Product - -_DEPENDENCIES = {"data_0": BLD / "data_0.pkl", "data_1": BLD / "data_1.pkl"} -_PRODUCTS = {"plot_0": BLD / "plot_0.png", "plot_1": BLD / "plot_1.png"} - - -def task_plot_data( - path_to_data: dict[str, Path] = _DEPENDENCIES, - path_to_plots: Annotated[dict[str, Path], Product] = _PRODUCTS, -) -> None: ... diff --git a/docs_src/tutorials/defining_dependencies_products_products_py38.py b/docs_src/tutorials/defining_dependencies_products_products_py38.py deleted file mode 100644 index c265f846..00000000 --- a/docs_src/tutorials/defining_dependencies_products_products_py38.py +++ /dev/null @@ -1,23 +0,0 @@ -from pathlib import Path -from typing import Annotated - -import numpy as np -import pandas as pd -from my_project.config import BLD - -from pytask import Product - - -def task_create_random_data( - path_to_data: Annotated[Path, Product] = BLD / "data.pkl", -) -> None: - rng = np.random.default_rng(0) - beta = 2 - - x = rng.normal(loc=5, scale=10, size=1_000) - epsilon = rng.standard_normal(1_000) - - y = beta * x + epsilon - - df = pd.DataFrame({"x": x, "y": y}) - df.to_pickle(path_to_data) diff --git a/docs_src/tutorials/defining_dependencies_products_relative_py38.py b/docs_src/tutorials/defining_dependencies_products_relative_py38.py deleted file mode 100644 index 731586b0..00000000 --- a/docs_src/tutorials/defining_dependencies_products_relative_py38.py +++ /dev/null @@ -1,9 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from pytask import Product - - -def task_create_random_data( - path_to_data: Annotated[Path, Product] = Path("../bld/data.pkl"), -) -> None: ... diff --git a/docs_src/tutorials/repeating_tasks_with_different_inputs1_py38.py b/docs_src/tutorials/repeating_tasks_with_different_inputs1_py38.py deleted file mode 100644 index 7d43d06a..00000000 --- a/docs_src/tutorials/repeating_tasks_with_different_inputs1_py38.py +++ /dev/null @@ -1,12 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from pytask import Product -from pytask import task - -for seed in range(10): - - @task - def task_create_random_data( - path: Annotated[Path, Product] = Path(f"data_{seed}.pkl"), seed: int = seed - ) -> None: ... diff --git a/docs_src/tutorials/repeating_tasks_with_different_inputs2_py38.py b/docs_src/tutorials/repeating_tasks_with_different_inputs2_py38.py deleted file mode 100644 index 7a7f8ec2..00000000 --- a/docs_src/tutorials/repeating_tasks_with_different_inputs2_py38.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from my_project.config import SRC - -from pytask import Product -from pytask import task - -for seed in range(10): - - @task - def task_create_random_data( - path_to_parameters: Path = SRC / "parameters.yml", - path_to_data: Annotated[Path, Product] = Path(f"data_{seed}.pkl"), - seed: int = seed, - ) -> None: ... diff --git a/docs_src/tutorials/repeating_tasks_with_different_inputs3_py38.py b/docs_src/tutorials/repeating_tasks_with_different_inputs3_py38.py deleted file mode 100644 index a3fe36b6..00000000 --- a/docs_src/tutorials/repeating_tasks_with_different_inputs3_py38.py +++ /dev/null @@ -1,13 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from pytask import Product -from pytask import task - -for seed in ((0,), (1,)): - - @task - def task_create_random_data( - seed: tuple[int] = seed, - path_to_data: Annotated[Path, Product] = Path(f"data_{seed[0]}.pkl"), - ) -> None: ... diff --git a/docs_src/tutorials/repeating_tasks_with_different_inputs4_py38.py b/docs_src/tutorials/repeating_tasks_with_different_inputs4_py38.py deleted file mode 100644 index b878a07c..00000000 --- a/docs_src/tutorials/repeating_tasks_with_different_inputs4_py38.py +++ /dev/null @@ -1,13 +0,0 @@ -from pathlib import Path -from typing import Annotated - -from pytask import Product -from pytask import task - -for seed, id_ in ((0, "first"), (1, "second")): - - @task(id=id_) - def task_create_random_data( - seed: int = seed, - path_to_data: Annotated[Path, Product] = Path(f"data_{seed}.txt"), - ) -> None: ... diff --git a/docs_src/tutorials/repeating_tasks_with_different_inputs5_py38.py b/docs_src/tutorials/repeating_tasks_with_different_inputs5_py38.py deleted file mode 100644 index 13fea375..00000000 --- a/docs_src/tutorials/repeating_tasks_with_different_inputs5_py38.py +++ /dev/null @@ -1,25 +0,0 @@ -from pathlib import Path -from typing import Annotated -from typing import NamedTuple - -from pytask import Product -from pytask import task - - -class _Arguments(NamedTuple): - seed: int - path_to_data: Path - - -ID_TO_KWARGS = { - "first": _Arguments(seed=0, path_to_data=Path("data_0.pkl")), - "second": _Arguments(seed=1, path_to_data=Path("data_1.pkl")), -} - - -for id_, kwargs in ID_TO_KWARGS.items(): - - @task(id=id_, kwargs=kwargs) - def task_create_random_data( - seed: int, path_to_data: Annotated[Path, Product] - ) -> None: ... diff --git a/docs_src/tutorials/using_a_data_catalog_2_py38.py b/docs_src/tutorials/using_a_data_catalog_2_py38.py deleted file mode 100644 index ceb46b11..00000000 --- a/docs_src/tutorials/using_a_data_catalog_2_py38.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Annotated - -import numpy as np -import pandas as pd -from my_project.config import data_catalog - -from pytask import PickleNode -from pytask import Product - - -def task_create_random_data( - node: Annotated[PickleNode, Product] = data_catalog["data"], -) -> None: - rng = np.random.default_rng(0) - beta = 2 - - x = rng.normal(loc=5, scale=10, size=1_000) - epsilon = rng.standard_normal(1_000) - - y = beta * x + epsilon - - df = pd.DataFrame({"x": x, "y": y}) - node.save(df) diff --git a/docs_src/tutorials/using_a_data_catalog_3_py38.py b/docs_src/tutorials/using_a_data_catalog_3_py38.py deleted file mode 100644 index e64fb7f6..00000000 --- a/docs_src/tutorials/using_a_data_catalog_3_py38.py +++ /dev/null @@ -1,20 +0,0 @@ -from pathlib import Path -from typing import Annotated - -import matplotlib.pyplot as plt -import pandas as pd -from my_project.config import BLD -from my_project.config import data_catalog - -from pytask import Product - - -def task_plot_data( - df: Annotated[pd.DataFrame, data_catalog["data"]], - path_to_plot: Annotated[Path, Product] = BLD / "plot.png", -) -> None: - _, ax = plt.subplots() - df.plot(x="x", y="y", ax=ax, kind="scatter") - - plt.savefig(path_to_plot) - plt.close() diff --git a/docs_src/tutorials/using_a_data_catalog_5_py38.py b/docs_src/tutorials/using_a_data_catalog_5_py38.py deleted file mode 100644 index 45304a93..00000000 --- a/docs_src/tutorials/using_a_data_catalog_5_py38.py +++ /dev/null @@ -1,17 +0,0 @@ -from pathlib import Path -from typing import Annotated - -import pandas as pd -from my_project.config import data_catalog - -from pytask import PickleNode -from pytask import Product - - -def task_transform_csv( - path: Annotated[Path, data_catalog["csv"]], - node: Annotated[PickleNode, Product] = data_catalog["transformed_csv"], -) -> None: - df = pd.read_csv(path) - # ... transform the data - node.save(df) diff --git a/docs_src/tutorials/write_a_task_py38.py b/docs_src/tutorials/write_a_task_py38.py deleted file mode 100644 index 20c2b321..00000000 --- a/docs_src/tutorials/write_a_task_py38.py +++ /dev/null @@ -1,22 +0,0 @@ -# Content of task_data_preparation.py. -from pathlib import Path -from typing import Annotated - -import numpy as np -import pandas as pd -from my_project.config import BLD - -from pytask import Product - - -def task_create_random_data(path: Annotated[Path, Product] = BLD / "data.pkl") -> None: - rng = np.random.default_rng(0) - beta = 2 - - x = rng.normal(loc=5, scale=10, size=1_000) - epsilon = rng.standard_normal(1_000) - - y = beta * x + epsilon - - df = pd.DataFrame({"x": x, "y": y}) - df.to_pickle(path) diff --git a/justfile b/justfile index a35931aa..bc00ad27 100644 --- a/justfile +++ b/justfile @@ -39,8 +39,8 @@ docs-serve: # Run tests with lowest dependency resolution (like CI) test-lowest: - uv run --group test --resolution lowest-direct pytest --nbmake -n auto + uv run --python 3.10 --group test --resolution lowest-direct pytest --nbmake # Run tests with highest dependency resolution (like CI) test-highest: - uv run --group test --resolution highest pytest --nbmake -n auto + uv run --python 3.13 --group test --resolution highest pytest --nbmake -n auto diff --git a/pyproject.toml b/pyproject.toml index 17d9f8fb..e88c6dee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "pytask" description = "pytask is a workflow management system that facilitates reproducible data analyses." -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", @@ -11,7 +11,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -47,7 +46,7 @@ name = "Tobias Raabe" email = "raabe@posteo.de" [dependency-groups] -dev = ["pygraphviz>=1.11;platform_system=='Linux'"] +dev = ["pygraphviz>=1.12;platform_system=='Linux'"] docs = [ "furo>=2024.8.6", "ipython>=8.13.2", diff --git a/src/_pytask/_inspect.py b/src/_pytask/_inspect.py index 4a567a69..98a94702 100644 --- a/src/_pytask/_inspect.py +++ b/src/_pytask/_inspect.py @@ -1,143 +1,6 @@ from __future__ import annotations -import functools -import sys -import types -from typing import TYPE_CHECKING -from typing import Any -from typing import Callable - -if TYPE_CHECKING: - from collections.abc import Mapping - __all__ = ["get_annotations"] -if sys.version_info >= (3, 10): # pragma: no cover - from inspect import get_annotations -else: # pragma: no cover - - def get_annotations( # noqa: C901, PLR0912, PLR0915 - obj: Callable[..., object] | type[Any] | types.ModuleType, - *, - globals: Mapping[str, Any] | None = None, # noqa: A002 - locals: Mapping[str, Any] | None = None, # noqa: A002 - eval_str: bool = False, - ) -> dict[str, Any]: - """Compute the annotations dict for an object. - - obj may be a callable, class, or module. - Passing in an object of any other type raises TypeError. - - Returns a dict. get_annotations() returns a new dict every time - it's called; calling it twice on the same object will return two - different but equivalent dicts. - - This function handles several details for you: - - * If eval_str is true, values of type str will - be un-stringized using eval(). This is intended - for use with stringized annotations - ("from __future__ import annotations"). - * If obj doesn't have an annotations dict, returns an - empty dict. (Functions and methods always have an - annotations dict; classes, modules, and other types of - callables may not.) - * Ignores inherited annotations on classes. If a class - doesn't have its own annotations dict, returns an empty dict. - * All accesses to object members and dict values are done - using getattr() and dict.get() for safety. - * Always, always, always returns a freshly-created dict. - - eval_str controls whether or not values of type str are replaced - with the result of calling eval() on those values: - - * If eval_str is true, eval() is called on values of type str. - * If eval_str is false (the default), values of type str are unchanged. - - globals and locals are passed in to eval(); see the documentation - for eval() for more information. If either globals or locals is - None, this function may replace that value with a context-specific - default, contingent on type(obj): - - * If obj is a module, globals defaults to obj.__dict__. - * If obj is a class, globals defaults to - sys.modules[obj.__module__].__dict__ and locals - defaults to the obj class namespace. - * If obj is a callable, globals defaults to obj.__globals__, - although if obj is a wrapped function (using - functools.update_wrapper()) it is first unwrapped. - """ - if isinstance(obj, type): - # class - obj_dict = getattr(obj, "__dict__", None) - if obj_dict and hasattr(obj_dict, "get"): - ann = obj_dict.get("__annotations__", None) - if isinstance(ann, types.GetSetDescriptorType): - ann = None - else: - ann = None - - obj_globals = None - module_name = getattr(obj, "__module__", None) - if module_name: - module = sys.modules.get(module_name, None) - if module: - obj_globals = getattr(module, "__dict__", None) - obj_locals = dict(vars(obj)) - unwrap = obj - elif isinstance(obj, types.ModuleType): - # module - ann = getattr(obj, "__annotations__", None) - obj_globals = obj.__dict__ - obj_locals = None - unwrap = None - elif callable(obj): - # this includes types.Function, types.BuiltinFunctionType, - # types.BuiltinMethodType, functools.partial, functools.singledispatch, - # "class funclike" from Lib/test/test_inspect... on and on it goes. - ann = getattr(obj, "__annotations__", None) - obj_globals = getattr(obj, "__globals__", None) - obj_locals = None - unwrap = obj - else: - msg = f"{obj!r} is not a module, class, or callable." - raise TypeError(msg) - - if ann is None: - return {} - - if not isinstance(ann, dict): - msg = f"{obj!r}.__annotations__ is neither a dict nor None" - raise ValueError(msg) # noqa: TRY004 - - if not ann: - return {} - - if not eval_str: - return dict(ann) - - if unwrap is not None: - while True: - if hasattr(unwrap, "__wrapped__"): - unwrap = unwrap.__wrapped__ - continue - if isinstance(unwrap, functools.partial): - unwrap = unwrap.func - continue - break - if hasattr(unwrap, "__globals__"): - obj_globals = unwrap.__globals__ - - if globals is None: - globals = obj_globals # noqa: A001 - if locals is None: - locals = obj_locals # noqa: A001 - - eval_func = eval - return { - key: value - if not isinstance(value, str) - else eval_func(value, globals, locals) - for key, value in ann.items() - } +from inspect import get_annotations diff --git a/src/_pytask/build.py b/src/_pytask/build.py index 78426cfd..ea242b59 100644 --- a/src/_pytask/build.py +++ b/src/_pytask/build.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any -from typing import Callable from typing import Literal import click @@ -35,6 +34,7 @@ from _pytask.traceback import Traceback if TYPE_CHECKING: + from collections.abc import Callable from collections.abc import Iterable from typing import NoReturn diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index b8e280a1..5c122833 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -6,14 +6,17 @@ import hashlib import inspect from inspect import FullArgSpec +from typing import TYPE_CHECKING from typing import Any -from typing import Callable from attrs import define from attrs import field from _pytask._hashlib import hash_value +if TYPE_CHECKING: + from collections.abc import Callable + @define class CacheInfo: diff --git a/src/_pytask/coiled_utils.py b/src/_pytask/coiled_utils.py index 41614681..7643933b 100644 --- a/src/_pytask/coiled_utils.py +++ b/src/_pytask/coiled_utils.py @@ -1,10 +1,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING from typing import Any -from typing import Callable from attrs import define +if TYPE_CHECKING: + from collections.abc import Callable + try: from coiled.function import Function except ImportError: diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index dab2e223..23451b2c 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING from typing import Annotated from typing import Any -from typing import Callable from typing import get_origin import attrs @@ -25,6 +24,7 @@ from _pytask.typing import no_default if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path from _pytask.session import Session diff --git a/src/_pytask/console.py b/src/_pytask/console.py index fb8bd302..a0082f4b 100644 --- a/src/_pytask/console.py +++ b/src/_pytask/console.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any -from typing import Callable from rich.console import Console from rich.console import RenderableType @@ -29,6 +28,7 @@ from _pytask.path import shorten_path if TYPE_CHECKING: + from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence from enum import Enum diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index 8127735f..e90079e4 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -216,7 +216,7 @@ def _shorten_node_labels(dag: nx.DiGraph, paths: list[Path]) -> nx.DiGraph: node_names = dag.nodes short_names = reduce_names_of_multiple_nodes(node_names, dag, paths) short_names = [i.plain if isinstance(i, Text) else i for i in short_names] # type: ignore[attr-defined] - old_to_new = dict(zip(node_names, short_names)) + old_to_new = dict(zip(node_names, short_names, strict=False)) return nx.relabel_nodes(dag, old_to_new) diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 64ba004e..3622562d 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -287,7 +287,7 @@ def pytask_execute_task(session: Session, task: PTask) -> bool: nodes = tree_leaves(task.produces["return"]) values = structure_return.flatten_up_to(out) - for node, value in zip(nodes, values): + for node, value in zip(nodes, values, strict=False): if not isinstance(node, PProvisionalNode): node.save(value) diff --git a/src/_pytask/mark/expression.py b/src/_pytask/mark/expression.py index 06700a26..20d3f902 100644 --- a/src/_pytask/mark/expression.py +++ b/src/_pytask/mark/expression.py @@ -27,11 +27,11 @@ import ast import enum import re +from collections.abc import Callable from collections.abc import Iterator from collections.abc import Mapping from collections.abc import Sequence from typing import TYPE_CHECKING -from typing import Callable from attrs import define diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index d3762e55..76b5e2a8 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -3,7 +3,6 @@ import warnings from typing import TYPE_CHECKING from typing import Any -from typing import Callable from attrs import define from attrs import field @@ -14,6 +13,7 @@ from _pytask.typing import is_task_function if TYPE_CHECKING: + from collections.abc import Callable from collections.abc import Iterable from collections.abc import Mapping diff --git a/src/_pytask/models.py b/src/_pytask/models.py index 791dbb06..7511f3e9 100644 --- a/src/_pytask/models.py +++ b/src/_pytask/models.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING from typing import Any -from typing import Callable from typing import NamedTuple from uuid import UUID from uuid import uuid4 @@ -13,6 +12,7 @@ from attrs import field if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path from _pytask.mark import Mark diff --git a/src/_pytask/node_protocols.py b/src/_pytask/node_protocols.py index 56b3ab8f..92589c56 100644 --- a/src/_pytask/node_protocols.py +++ b/src/_pytask/node_protocols.py @@ -3,11 +3,11 @@ import warnings from typing import TYPE_CHECKING from typing import Any -from typing import Callable from typing import Protocol from typing import runtime_checkable if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path from _pytask.mark import Mark diff --git a/src/_pytask/nodes.py b/src/_pytask/nodes.py index 999e1e9d..4a442547 100644 --- a/src/_pytask/nodes.py +++ b/src/_pytask/nodes.py @@ -10,7 +10,6 @@ from pathlib import Path # noqa: TC003 from typing import TYPE_CHECKING from typing import Any -from typing import Callable from attrs import define from attrs import field @@ -29,6 +28,7 @@ from _pytask.typing import no_default if TYPE_CHECKING: + from collections.abc import Callable from io import BufferedReader from io import BufferedWriter diff --git a/src/_pytask/persist.py b/src/_pytask/persist.py index 59e06fab..9bd567b4 100644 --- a/src/_pytask/persist.py +++ b/src/_pytask/persist.py @@ -58,7 +58,9 @@ def pytask_execute_task_setup(session: Session, task: PTask) -> None: state=state, ) for name, state in zip( - node_and_neighbors(session.dag, task.signature), all_states + node_and_neighbors(session.dag, task.signature), + all_states, + strict=False, ) ) if any_node_changed: diff --git a/src/_pytask/profile.py b/src/_pytask/profile.py index 60884708..95dc82e2 100644 --- a/src/_pytask/profile.py +++ b/src/_pytask/profile.py @@ -201,7 +201,9 @@ def _collect_runtimes(tasks: list[PTask]) -> dict[str, float]: """Collect runtimes.""" with DatabaseSession() as session: runtimes = [session.get(Runtime, task.signature) for task in tasks] - return {task.name: r.duration for task, r in zip(tasks, runtimes) if r} + return { + task.name: r.duration for task, r in zip(tasks, runtimes, strict=False) if r + } class FileSizeNameSpace: diff --git a/src/_pytask/provisional.py b/src/_pytask/provisional.py index 3c8e59dd..2e582bc2 100644 --- a/src/_pytask/provisional.py +++ b/src/_pytask/provisional.py @@ -6,7 +6,6 @@ import sys from typing import TYPE_CHECKING from typing import Any -from typing import Callable from _pytask.config import hookimpl from _pytask.exceptions import NodeLoadError @@ -27,6 +26,7 @@ from pytask import TaskOutcome if TYPE_CHECKING: + from collections.abc import Callable from collections.abc import Mapping from _pytask.session import Session diff --git a/src/_pytask/shared.py b/src/_pytask/shared.py index 7cfd62d6..770a6b8c 100644 --- a/src/_pytask/shared.py +++ b/src/_pytask/shared.py @@ -4,12 +4,12 @@ import glob import inspect +from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING from typing import Any -from typing import Callable import click diff --git a/src/_pytask/task.py b/src/_pytask/task.py index 902f6eb0..117af08e 100644 --- a/src/_pytask/task.py +++ b/src/_pytask/task.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING from typing import Any -from typing import Callable from _pytask.console import format_strings_as_flat_tree from _pytask.pluginmanager import hookimpl @@ -13,6 +12,7 @@ from _pytask.task_utils import parse_collected_tasks_with_task_marker if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path from _pytask.reports import CollectionReport diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index 4fd221ec..b10176a3 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -8,7 +8,6 @@ from types import BuiltinFunctionType from typing import TYPE_CHECKING from typing import Any -from typing import Callable import attrs @@ -22,6 +21,7 @@ from _pytask.typing import is_task_function if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index d2b13222..9e1435af 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -6,7 +6,6 @@ from types import TracebackType from typing import TYPE_CHECKING from typing import ClassVar -from typing import Union import pluggy from attrs import define @@ -19,11 +18,11 @@ if TYPE_CHECKING: from collections.abc import Generator + from typing import TypeAlias from rich.console import Console from rich.console import ConsoleOptions from rich.console import RenderResult - from typing_extensions import TypeAlias __all__ = [ @@ -37,9 +36,9 @@ ExceptionInfo: TypeAlias = tuple[ - type[BaseException], BaseException, Union[TracebackType, None] + type[BaseException], BaseException, TracebackType | None ] -OptionalExceptionInfo: TypeAlias = Union[ExceptionInfo, tuple[None, None, None]] +OptionalExceptionInfo: TypeAlias = ExceptionInfo | tuple[None, None, None] @define diff --git a/src/_pytask/typing.py b/src/_pytask/typing.py index f2ddba9b..d433ea06 100644 --- a/src/_pytask/typing.py +++ b/src/_pytask/typing.py @@ -10,7 +10,7 @@ from attrs import define if TYPE_CHECKING: - from typing_extensions import TypeAlias + from typing import TypeAlias from pytask import PTask diff --git a/tests/test_capture.py b/tests/test_capture.py index 380b31aa..ba37be8b 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -750,11 +750,6 @@ def test_many(self, capfd): # noqa: ARG002 class TestStdCaptureFDinvalidFD: - @pytest.mark.skipif( - sys.platform == "darwin" and sys.version_info[:2] == (3, 9), - reason="Causes following tests to fail and kills the pytest session with exit " - "code 137.", - ) def test_stdcapture_fd_invalid_fd(self, tmp_path, runner): source = """ import os diff --git a/tests/test_dag_command.py b/tests/test_dag_command.py index 458546f3..3ca41baf 100644 --- a/tests/test_dag_command.py +++ b/tests/test_dag_command.py @@ -62,9 +62,6 @@ def task_example(path=Path("input.txt")): ... @pytest.mark.skipif(not _TEST_SHOULD_RUN, reason="pygraphviz is required") -@pytest.mark.xfail( - sys.platform == "linux" and sys.version_info[:2] == (3, 9), reason="flakey" -) @pytest.mark.parametrize("layout", _GRAPH_LAYOUTS) @pytest.mark.parametrize("format_", _TEST_FORMATS) @pytest.mark.parametrize("rankdir", [_RankDirection.LR.value, _RankDirection.TB]) diff --git a/tests/test_execute.py b/tests/test_execute.py index e1be71a8..1a233160 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -638,7 +638,7 @@ def task2() -> None: pass @pytest.mark.xfail( - sys.platform == "linux" and sys.version_info[:2] == (3, 9), reason="flakey" + sys.platform == "linux" and sys.version_info[:2] == (3, 10), reason="flakey" ) def test_pytask_on_a_module_that_uses_the_functional_api(tmp_path): source = """ diff --git a/tests/test_mark_expression.py b/tests/test_mark_expression.py index ac1b2dad..5cb893be 100644 --- a/tests/test_mark_expression.py +++ b/tests/test_mark_expression.py @@ -1,12 +1,15 @@ from __future__ import annotations -from typing import Callable +from typing import TYPE_CHECKING import pytest from _pytask.mark.expression import Expression from _pytask.mark.expression import ParseError +if TYPE_CHECKING: + from collections.abc import Callable + def evaluate(input_: str, matcher: Callable[[str], bool]) -> bool: return Expression.compile_(input_).evaluate(matcher)