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)