pyplotutil 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. pyplotutil-2.0.0/.claude/settings.json +17 -0
  2. pyplotutil-2.0.0/.claude/skills/check/SKILL.md +17 -0
  3. pyplotutil-2.0.0/.github/dependabot.yml +19 -0
  4. pyplotutil-2.0.0/.github/workflows/ci.yml +80 -0
  5. pyplotutil-2.0.0/.github/workflows/release.yml +35 -0
  6. pyplotutil-2.0.0/.gitignore +120 -0
  7. pyplotutil-2.0.0/.python-version +1 -0
  8. pyplotutil-2.0.0/CLAUDE.md +27 -0
  9. pyplotutil-2.0.0/LICENSE +21 -0
  10. pyplotutil-2.0.0/PKG-INFO +123 -0
  11. pyplotutil-2.0.0/README.md +88 -0
  12. pyplotutil-2.0.0/examples/__init__.py +3 -0
  13. pyplotutil-2.0.0/examples/data/model_010.csv +202 -0
  14. pyplotutil-2.0.0/examples/data/model_015.csv +202 -0
  15. pyplotutil-2.0.0/examples/data/model_020.csv +202 -0
  16. pyplotutil-2.0.0/examples/data/model_030.csv +202 -0
  17. pyplotutil-2.0.0/examples/data/model_050.csv +202 -0
  18. pyplotutil-2.0.0/examples/data/model_100.csv +202 -0
  19. pyplotutil-2.0.0/examples/example.py +164 -0
  20. pyplotutil-2.0.0/examples/make_data.py +55 -0
  21. pyplotutil-2.0.0/examples/tagged_data/models.csv +1207 -0
  22. pyplotutil-2.0.0/noxfile.py +46 -0
  23. pyplotutil-2.0.0/pyproject.toml +222 -0
  24. pyplotutil-2.0.0/src/pyplotutil/__init__.py +70 -0
  25. pyplotutil-2.0.0/src/pyplotutil/_typing.py +38 -0
  26. pyplotutil-2.0.0/src/pyplotutil/datautil.py +1295 -0
  27. pyplotutil-2.0.0/src/pyplotutil/loggingutil.py +743 -0
  28. pyplotutil-2.0.0/src/pyplotutil/plotutil.py +846 -0
  29. pyplotutil-2.0.0/src/pyplotutil/py.typed +0 -0
  30. pyplotutil-2.0.0/tests/__init__.py +1 -0
  31. pyplotutil-2.0.0/tests/data/data1.csv +4 -0
  32. pyplotutil-2.0.0/tests/data/data2.csv +5 -0
  33. pyplotutil-2.0.0/tests/data/data3.csv +6 -0
  34. pyplotutil-2.0.0/tests/data/test.csv +5 -0
  35. pyplotutil-2.0.0/tests/data/test_comment_header.csv +5 -0
  36. pyplotutil-2.0.0/tests/data/test_comment_partial_data.csv +5 -0
  37. pyplotutil-2.0.0/tests/data/test_no_header.csv +4 -0
  38. pyplotutil-2.0.0/tests/data/test_spaces.txt +5 -0
  39. pyplotutil-2.0.0/tests/data/test_tagged_data.csv +19 -0
  40. pyplotutil-2.0.0/tests/data/test_tagged_data_label.csv +19 -0
  41. pyplotutil-2.0.0/tests/test_datautil.py +1013 -0
  42. pyplotutil-2.0.0/tests/test_loggingutil.py +454 -0
  43. pyplotutil-2.0.0/tests/test_package.py +40 -0
  44. pyplotutil-2.0.0/tests/test_plotutil.py +659 -0
  45. pyplotutil-2.0.0/uv.lock +1054 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "jq -r '.tool_input.file_path // .tool_response.filePath // empty' | { read -r f; case \"$f\" in *.py) uv run --no-sync --project \"${CLAUDE_PROJECT_DIR:-.}\" ruff format \"$f\" 2>/dev/null || true;; esac; }",
10
+ "timeout": 30,
11
+ "statusMessage": "Formatting with ruff..."
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: check
3
+ description: Run the full quality gate for pyplotutil — ruff lint, mypy type check, and the pytest suite. Use before committing or when the user asks to verify the codebase is green.
4
+ ---
5
+
6
+ Run each step with Bash and report results. Continue through all steps even if an earlier one fails, so the user gets the complete picture in one pass.
7
+
8
+ 1. **Lint**: `uv run ruff check`
9
+ 2. **Format check**: `uv run ruff format --check`
10
+ 3. **Type check**: `uv run mypy`
11
+ 4. **Tests**: `uv run pytest`
12
+
13
+ Then summarize: one line per step (pass/fail), followed by details for any failures.
14
+
15
+ - If ruff reports fixable issues, offer to run `uv run ruff check --fix` (note: `SIM105` is configured unfixable).
16
+ - mypy failures are expected to be fixed, not ignored — CI enforces type checking via `nox -s typecheck`.
17
+ - The test suite uses pytest-randomly; if a failure looks order-dependent, rerun with the printed seed (`-p randomly --randomly-seed=<seed>`) to reproduce.
@@ -0,0 +1,19 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "monthly"
7
+ groups:
8
+ actions:
9
+ patterns:
10
+ - "*"
11
+
12
+ - package-ecosystem: "uv"
13
+ directory: "/"
14
+ schedule:
15
+ interval: "monthly"
16
+
17
+ # Local Variables:
18
+ # jinx-local-words: "dependabot github uv"
19
+ # End:
@@ -0,0 +1,80 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop, "ci/**"]
6
+ paths-ignore:
7
+ - "README.md"
8
+ tags:
9
+ - "*"
10
+ pull_request:
11
+ branches: [main, develop]
12
+ workflow_dispatch:
13
+
14
+ concurrency:
15
+ group: ${{ github.workflow }}-${{ github.ref }}
16
+ cancel-in-progress: true
17
+
18
+ defaults:
19
+ run:
20
+ shell: bash
21
+
22
+ env:
23
+ LANG: en_US.utf-8
24
+ LC_ALL: en_US.utf-8
25
+ PYTHONIOENCODING: UTF-8
26
+ FORCE_COLOR: 3
27
+
28
+ jobs:
29
+ checks:
30
+ name: checks
31
+ runs-on: ubuntu-latest
32
+
33
+ steps:
34
+ - name: Checkout
35
+ uses: actions/checkout@v7
36
+ with:
37
+ fetch-depth: 0
38
+
39
+ - name: Setup uv
40
+ uses: astral-sh/setup-uv@v8.3.2
41
+ with:
42
+ enable-cache: true
43
+
44
+ - name: Run lint and type check
45
+ run: uvx --from 'nox[uv]' nox --non-interactive --error-on-missing-interpreters --session lint typecheck
46
+
47
+ tests:
48
+ timeout-minutes: 10
49
+ strategy:
50
+ max-parallel: 4
51
+ matrix:
52
+ os:
53
+ - ubuntu-22.04
54
+ - ubuntu-24.04
55
+ # - macos-latest
56
+ # - windows-latest
57
+ python-version:
58
+ - "3.11"
59
+ - "3.12"
60
+ - "3.13"
61
+ - "3.14"
62
+ runs-on: ${{ matrix.os }}
63
+
64
+ steps:
65
+ - name: Checkout
66
+ uses: actions/checkout@v7
67
+ with:
68
+ fetch-depth: 0
69
+
70
+ - name: Setup uv
71
+ uses: astral-sh/setup-uv@v8.3.2
72
+ with:
73
+ enable-cache: true
74
+
75
+ - name: Run tests on ${{ matrix.os }}
76
+ run: uvx --from 'nox[uv]' nox --non-interactive --error-on-missing-interpreters --session "tests-${{ matrix.python-version }}" -- --full-trace --cov
77
+
78
+ # Local Variables:
79
+ # jinx-local-words: "ci github macos md nox os typecheck ubuntu uv uvx"
80
+ # End:
@@ -0,0 +1,35 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ pypi:
10
+ name: Publish to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/project/pyplotutil/
15
+ permissions:
16
+ id-token: write
17
+
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v7
21
+ with:
22
+ fetch-depth: 0
23
+
24
+ - name: Setup uv
25
+ uses: astral-sh/setup-uv@v8.3.2
26
+
27
+ - name: Build distributions
28
+ run: uv build
29
+
30
+ - name: Publish to PyPI
31
+ run: uv publish
32
+
33
+ # Local Variables:
34
+ # jinx-local-words: "pypi ubuntu uv"
35
+ # End:
@@ -0,0 +1,120 @@
1
+ /example.log
2
+
3
+ ### Python ###
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+ # C extensions
10
+ *.so
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py,cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Sphinx documentation
58
+ docs/_build/
59
+
60
+ # PyBuilder
61
+ .pybuilder/
62
+ target/
63
+
64
+ # Jupyter Notebook
65
+ .ipynb_checkpoints
66
+
67
+ # IPython
68
+ profile_default/
69
+ ipython_config.py
70
+
71
+ # pyenv
72
+ # For a library or package, you might want to ignore these files since the code is
73
+ # intended to run in multiple environments; otherwise, check them in:
74
+ # .python-version
75
+
76
+ # pipenv
77
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
78
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
79
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
80
+ # install all needed dependencies.
81
+ #Pipfile.lock
82
+
83
+ # poetry
84
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
85
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
86
+ # commonly ignored for libraries.
87
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
88
+ #poetry.lock
89
+
90
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
91
+ __pypackages__/
92
+
93
+ # pdm
94
+ .pdm.toml
95
+
96
+ # Environments
97
+ .env
98
+ .venv
99
+ env/
100
+ venv/
101
+ ENV/
102
+ env.bak/
103
+ venv.bak/
104
+
105
+ # mypy
106
+ .mypy_cache/
107
+ .dmypy.json
108
+ dmypy.json
109
+
110
+ # Pyre type checker
111
+ .pyre/
112
+
113
+ # pytype static type analyzer
114
+ .pytype/
115
+
116
+ # Cython debug symbols
117
+ cython_debug/
118
+
119
+ # Claude Code personal settings
120
+ .claude/settings.local.json
@@ -0,0 +1 @@
1
+ 3.11.15
@@ -0,0 +1,27 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ - Package manager is **uv**; run tools through it: `uv run pytest`, `uv run mypy src tests`, `uv run ruff check`.
8
+ - `nox` runs the default sessions (lint, typecheck, tests across Python 3.11–3.14). Prefer `uv run pytest` for quick iteration.
9
+ - Run a single test: `uv run pytest -k 'test_name'`.
10
+ - Run `uv run mypy` before finishing a change (targets `src`, `tests`, and `examples` via config); CI enforces it through `nox -s typecheck`.
11
+
12
+ ## Code style
13
+
14
+ - ruff has `select = ["ALL"]` (every rule enabled), line length 120, numpy docstring convention.
15
+ - Every module must start with `from __future__ import annotations` (ruff isort `required-imports`).
16
+ - Format with `ruff format` (Black-compatible, double quotes).
17
+
18
+ ## Gotchas
19
+
20
+ - numpy requires `>=2.2.0`. A historical `<2.2.0` pin was confirmed obsolete in July 2026 (full matrix green on numpy 2.2.6/2.5.1) — don't reintroduce it without a reproducible failure.
21
+ - Data handling is built on **polars**, not pandas.
22
+ - The package version comes from git tags via hatch-vcs — do not add a static `version` to pyproject.toml.
23
+ - pytest-randomly randomizes test order, so tests must not depend on execution order.
24
+
25
+ ## Commits
26
+
27
+ - Conventional Commits with a capitalized, imperative subject: `feat: Add ...`, `fix: Handle ...`, `chore: Re-lock ...`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Hiroshi Atsuta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyplotutil
3
+ Version: 2.0.0
4
+ Summary: Python utilities for academic plotting
5
+ Project-URL: Homepage, https://github.com/hrshtst/pyplotutil
6
+ Author-email: Hiroshi Atsuta <atsuta@ieee.org>
7
+ Maintainer-email: Hiroshi Atsuta <atsuta@ieee.org>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: matplotlib-figures,matplotlib-style-sheets,matplotlib-styles,python,scientific-papers
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Matplotlib
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: Scientific/Engineering :: Visualization
25
+ Classifier: Topic :: Utilities
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Requires-Dist: matplotlib>=3.10.0
29
+ Requires-Dist: numpy>=2.2.0
30
+ Requires-Dist: polars>=1.17.1
31
+ Requires-Dist: scienceplots>=2.1.1
32
+ Provides-Extra: gui
33
+ Requires-Dist: pyqt6>=6.8.0; extra == 'gui'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # pyplotutil: plotting utility for academic publications
37
+
38
+ **pyplotutil** is a wrapper library of
39
+ [polars](https://pola.rs/) and
40
+ [matplotlib](https://matplotlib.org/), which allows you to handle time
41
+ series data easily and create graphs tolerable to scientific
42
+ publications.
43
+
44
+ ## Features
45
+
46
+ - Load tabular data from CSV files, buffers, or polars frames with
47
+ column-attribute access (`data.time`, `data.voltage`)
48
+ - Group rows by a tag column (`TaggedData`) or manage whole
49
+ directories of data files (`Dataset`)
50
+ - Plot multiple time series and mean-with-error graphs (standard
51
+ deviation, standard error, variance, range)
52
+ - Apply publication-ready matplotlib styles
53
+ ([SciencePlots](https://github.com/garrettj403/SciencePlots)):
54
+ `science`, `ieee`, `nature`, `notebook`
55
+ - Save figures to multiple formats in one call with sanitized
56
+ filenames
57
+ - Event logging helpers with file and console output
58
+
59
+ ## Installation
60
+
61
+ Requires Python 3.11+.
62
+
63
+ ```console
64
+ pip install git+https://github.com/hrshtst/pyplotutil.git
65
+ ```
66
+
67
+ Interactive plotting with `plt.show()` needs a GUI backend, which is
68
+ provided by the optional `gui` extra:
69
+
70
+ ```console
71
+ pip install "pyplotutil[gui] @ git+https://github.com/hrshtst/pyplotutil.git"
72
+ ```
73
+
74
+ ## Quickstart
75
+
76
+ ```python
77
+ import matplotlib.pyplot as plt
78
+
79
+ from pyplotutil import Data, Dataset, apply_style, plot_mean_err, save_figure
80
+
81
+ # Publication-ready style.
82
+ apply_style("science", no_latex=True)
83
+
84
+ # Load a single CSV file; columns are accessible as attributes.
85
+ data = Data("experiment.csv")
86
+ fig, ax = plt.subplots()
87
+ ax.plot(data.t, data.position)
88
+
89
+ # Load every CSV file in a directory and plot mean with standard error.
90
+ dataset = Dataset("results/")
91
+ t, y = dataset.get_timeseries("position")
92
+ plot_mean_err(ax, t, y, "se", tlim=None, lw=1, capsize=2, label="mean")
93
+
94
+ # Write figure.png and figure.pdf in one call.
95
+ save_figure(fig, "output", "figure", ["png", "pdf"])
96
+ ```
97
+
98
+ Group rows by a tag column:
99
+
100
+ ```python
101
+ from pyplotutil import TaggedData
102
+
103
+ tagged = TaggedData("trials.csv", tag_column="trial")
104
+ for tag, data in tagged:
105
+ print(tag, data.param("gain"))
106
+ ```
107
+
108
+ ## Development
109
+
110
+ This project uses [uv](https://docs.astral.sh/uv/) and
111
+ [nox](https://nox.thea.codes/):
112
+
113
+ ```console
114
+ uv sync # set up the development environment
115
+ uv run pytest # run the test suite
116
+ uv run mypy # type check
117
+ uv run ruff check # lint
118
+ nox # lint + type check + tests on Python 3.11-3.14
119
+ ```
120
+
121
+ ## License
122
+
123
+ [MIT](LICENSE)
@@ -0,0 +1,88 @@
1
+ # pyplotutil: plotting utility for academic publications
2
+
3
+ **pyplotutil** is a wrapper library of
4
+ [polars](https://pola.rs/) and
5
+ [matplotlib](https://matplotlib.org/), which allows you to handle time
6
+ series data easily and create graphs tolerable to scientific
7
+ publications.
8
+
9
+ ## Features
10
+
11
+ - Load tabular data from CSV files, buffers, or polars frames with
12
+ column-attribute access (`data.time`, `data.voltage`)
13
+ - Group rows by a tag column (`TaggedData`) or manage whole
14
+ directories of data files (`Dataset`)
15
+ - Plot multiple time series and mean-with-error graphs (standard
16
+ deviation, standard error, variance, range)
17
+ - Apply publication-ready matplotlib styles
18
+ ([SciencePlots](https://github.com/garrettj403/SciencePlots)):
19
+ `science`, `ieee`, `nature`, `notebook`
20
+ - Save figures to multiple formats in one call with sanitized
21
+ filenames
22
+ - Event logging helpers with file and console output
23
+
24
+ ## Installation
25
+
26
+ Requires Python 3.11+.
27
+
28
+ ```console
29
+ pip install git+https://github.com/hrshtst/pyplotutil.git
30
+ ```
31
+
32
+ Interactive plotting with `plt.show()` needs a GUI backend, which is
33
+ provided by the optional `gui` extra:
34
+
35
+ ```console
36
+ pip install "pyplotutil[gui] @ git+https://github.com/hrshtst/pyplotutil.git"
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ ```python
42
+ import matplotlib.pyplot as plt
43
+
44
+ from pyplotutil import Data, Dataset, apply_style, plot_mean_err, save_figure
45
+
46
+ # Publication-ready style.
47
+ apply_style("science", no_latex=True)
48
+
49
+ # Load a single CSV file; columns are accessible as attributes.
50
+ data = Data("experiment.csv")
51
+ fig, ax = plt.subplots()
52
+ ax.plot(data.t, data.position)
53
+
54
+ # Load every CSV file in a directory and plot mean with standard error.
55
+ dataset = Dataset("results/")
56
+ t, y = dataset.get_timeseries("position")
57
+ plot_mean_err(ax, t, y, "se", tlim=None, lw=1, capsize=2, label="mean")
58
+
59
+ # Write figure.png and figure.pdf in one call.
60
+ save_figure(fig, "output", "figure", ["png", "pdf"])
61
+ ```
62
+
63
+ Group rows by a tag column:
64
+
65
+ ```python
66
+ from pyplotutil import TaggedData
67
+
68
+ tagged = TaggedData("trials.csv", tag_column="trial")
69
+ for tag, data in tagged:
70
+ print(tag, data.param("gain"))
71
+ ```
72
+
73
+ ## Development
74
+
75
+ This project uses [uv](https://docs.astral.sh/uv/) and
76
+ [nox](https://nox.thea.codes/):
77
+
78
+ ```console
79
+ uv sync # set up the development environment
80
+ uv run pytest # run the test suite
81
+ uv run mypy # type check
82
+ uv run ruff check # lint
83
+ nox # lint + type check + tests on Python 3.11-3.14
84
+ ```
85
+
86
+ ## License
87
+
88
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ """Example scripts."""
2
+
3
+ from __future__ import annotations