diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 7c5d10f2..1f253faa 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' cache: 'poetry' - name: Install project run: poetry install -E all diff --git a/.github/workflows/python-lint-tests.yml b/.github/workflows/python-lint-tests.yml index 9282f3e8..3688f065 100644 --- a/.github/workflows/python-lint-tests.yml +++ b/.github/workflows/python-lint-tests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.11', '3.11', '3.13'] os: [ubuntu-latest, macos-latest, windows-latest] defaults: run: diff --git a/.readthedocs.yml b/.readthedocs.yml index 1b5e79b4..b58fecbf 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,7 @@ sphinx: build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.13" jobs: pre_create_environment: - pip install poetry diff --git a/CHANGELOG.md b/CHANGELOG.md index cea7d776..6fdae6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Release date: UNRELEASED ### New features and improvements +* Added support for Python 3.13 and dropped support for Python 3.10 (#784) * Added a `--orientation` option to the `pagerotate` command to conditionally rotate the page to a target orientation (thanks to @gatesphere) (#705) * Added a `lineshuffle` command to randomize the plotting order of lines in the current geometry (thanks to @gatesphere) (#715) diff --git a/README.md b/README.md index ab301a4d..1f6a4c7b 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ curvy (circles, bezier curves, etc.) to lines made of small segments. _vpype_ do Detailed installation instructions are available in the [latest documentation](https://vpype.readthedocs.io/en/latest/install.html). TL;DR: -- Python 3.12 is recommended, but *vpype* is also compatible with Python 3.10 and 3.11. +- Python 3.13 is recommended, but *vpype* is also compatible with Python 3.11 and 3.12. - *vpype* is published on the [Python Package Index](https://pypi.org) and can be installed using [pipx](https://pypa.github.io/pipx/): ```bash pipx install "vpype[all]" diff --git a/docs/install.rst b/docs/install.rst index 66cf5452..c4b0ed59 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -9,7 +9,7 @@ This page explain how to install *vpype* for end-users. If you intend to develop .. note:: - The recommended Python version is 3.12.1 or later. *vpype* is also compatible with Python 3.10 and 3.11. + The recommended Python version is 3.13. *vpype* is also compatible with Python 3.11 and 3.12. .. warning:: @@ -38,7 +38,7 @@ You can ensure that the installed Python interpreter is properly installed by r It should produce an output similar to:: - Python 3.12.1 + Python 3.13.2 The version number should match the installer you used. diff --git a/poetry.lock b/poetry.lock index 9e3f17f3..3c215ead 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,7 +37,6 @@ files = [ ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} @@ -405,9 +404,6 @@ files = [ {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -456,22 +452,6 @@ files = [ {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev", "docs"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "fonttools" version = "4.56.0" @@ -1142,7 +1122,6 @@ files = [ [package.dependencies] mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] @@ -1583,11 +1562,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -2028,7 +2005,6 @@ sphinxcontrib-htmlhelp = ">=2.0.6" sphinxcontrib-jsmath = ">=1.0.1" sphinxcontrib-qthelp = ">=1.0.6" sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] @@ -2279,7 +2255,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2396,7 +2372,6 @@ files = [ [package.dependencies] click = ">=7.0" h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] @@ -2569,5 +2544,5 @@ all = ["Pillow", "PySide6", "glcontext", "matplotlib", "moderngl"] [metadata] lock-version = "2.1" -python-versions = ">=3.10, <3.13" -content-hash = "dd37fe841bc52059bdd52237c8c41231f6edf4576543955a8b8bec7e4cd007d2" +python-versions = ">=3.11, <3.14" +content-hash = "c93b024a3bda5d47d33c48835bd876d0e4fe8d57a63dbd0ce3708facf0274692" diff --git a/pyproject.toml b/pyproject.toml index 1abdc068..a61ec692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,49 @@ -[tool.poetry] +[project] name = "vpype" version = "1.15.0a0" description = "The Swiss Army knife of vector graphics for pen plotters" -authors = ["Antoine Beyeler "] +authors = [ + { name = "Antoine Beyeler", email = "abeyeler@ab-ware.com>" } +] license = "MIT" readme = "README.md" -homepage = "https://github.com/abey79/vpype" -documentation = "https://vpype.readthedocs.io/en/latest/" -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Topic :: Artistic Software", - "Topic :: Multimedia :: Graphics", +requires-python = ">=3.11, <3.14" +dynamic = ["classifiers"] + +dependencies = [ + "asteval>=0.9.26", + "cachetools>=4.2.2", + "click>=8.0.1,<8.2.0", + "multiprocess>=0.70.11", + "numpy>=1.25,<3", + "pnoise>=0.2.0", + "pyphen>=0.14,<0.16", + "scipy>=1.6", + "Shapely>=1.8.2", + "svgelements>=1.6.10", + "svgwrite~=1.4", + "tomli>=2.0.0", +] + +[project.optional-dependencies] +all = [ + "matplotlib>=3.3.2", + "glcontext>=2.3.2", # 2.3.2 needed to fix #200 + "moderngl>=5.6.2,!=5.7.1,!=5.7.2", # see moderngl/moderngl#525 + "Pillow>=9.0.1", + "PySide6>=6.4.0.1,!=6.6.2", ] + + +[project.urls] +documentation = "https://vpype.readthedocs.io/en/latest/" +repository = "https://github.com/abey79/vpype" + +[project.scripts] +vpype = "vpype_cli.cli:cli" + + +[tool.poetry] packages = [ { include = "vpype" }, { include = "vpype_cli" }, @@ -29,31 +60,14 @@ include = [ "vpype_viewer/qtviewer/resources/*", ] -[tool.poetry.scripts] -vpype = "vpype_cli.cli:cli" +# Poetry autofills license and Python version related items. +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Topic :: Artistic Software", + "Topic :: Multimedia :: Graphics", +] -[tool.poetry.dependencies] -python = ">=3.10, <3.13" - -asteval = ">=0.9.26" -cachetools = ">=4.2.2" -click = ">=8.0.1,<8.2.0" -multiprocess = ">=0.70.11" -numpy = ">=1.25,<3" -pnoise = ">=0.2.0" -pyphen = ">=0.14,<0.16" -scipy = ">=1.6" -Shapely = ">=1.8.2" -svgelements = ">=1.6.10" -svgwrite = "~1.4" -tomli = ">=2.0.0" - -# additional dependencies for the viewer and the `show` command -matplotlib = { version = ">=3.3.2", optional = true } -glcontext = { version = ">=2.3.2", optional = true } # 2.3.2 needed to fix #200 -moderngl = { version = ">=5.6.2,!=5.7.1,!=5.7.2", optional = true } # see moderngl/moderngl#525 -Pillow = { version = ">=9.0.1", optional = true } -PySide6 = { version = ">=6.4.0.1,!=6.6.2", optional = true } [tool.poetry.group.dev.dependencies] coverage = {extras = ["toml"], version = ">=5.4"} @@ -83,9 +97,6 @@ sphinx-click = ">=4.3.0" sphinx-copybutton = ">=0.5.0" -[tool.poetry.extras] -all = ["matplotlib", "glcontext", "moderngl", "Pillow", "PySide6"] - [build-system] requires = ["poetry-core>=1.0.8"] build-backend = "poetry.core.masonry.api" @@ -129,13 +140,10 @@ exclude_lines = [ '@(abc\.)?abstractmethod', ] -[tool.black] -line-length = 95 -target-version = ["py39", "py310", "py311"] [tool.ruff] line-length = 95 -target-version = "py39" +target-version = "py311" exclude = ["examples", "scripts"] [tool.ruff.lint] diff --git a/tests/conftest.py b/tests/conftest.py index d3fae5aa..eb2d6575 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,8 @@ import pathlib import random import string -from typing import Callable, Protocol +from collections.abc import Callable +from typing import Protocol from xml.dom import minidom from xml.etree import ElementTree diff --git a/vpype/io.py b/vpype/io.py index 255ccb2c..91ed0cfa 100644 --- a/vpype/io.py +++ b/vpype/io.py @@ -84,11 +84,11 @@ def get(self) -> np.ndarray: return self._stack -_PathType = Union[ +_PathType = Union[ # noqa: UP007 # for actual paths and shapes transformed into paths svgelements.Path, # for the special case of Polygon and Polylines - list[Union[svgelements.PathSegment, svgelements.Polygon, svgelements.Polyline]], + list[svgelements.PathSegment | svgelements.Polygon | svgelements.Polyline], ] _PathListType = list[_PathType] @@ -178,7 +178,7 @@ def _element_to_paths(elem: svgelements.SVGElement) -> _PathType | None: if isinstance(elem, svgelements.Path): if len(elem) != 0: return elem - elif isinstance(elem, (svgelements.Polyline, svgelements.Polygon)): + elif isinstance(elem, svgelements.Polyline | svgelements.Polygon): # Here we add a "fake" path containing just the Polyline/Polygon, # to be treated specifically by _convert_flattened_paths. path = [svgelements.Move(elem.points[0]), elem] @@ -316,14 +316,14 @@ def _process_path(path): point_stack = _ComplexStack() point_stack.append(complex(seg.end)) - elif isinstance(seg, (svgelements.Line, svgelements.Close)): + elif isinstance(seg, svgelements.Line | svgelements.Close): start = complex(seg.start) end = complex(seg.end) if not point_stack.ends_with(start): point_stack.append(start) if end != start: point_stack.append(end) - elif isinstance(seg, (svgelements.Polygon, svgelements.Polyline)): + elif isinstance(seg, svgelements.Polygon | svgelements.Polyline): line = np.array(seg.points, dtype=float) line = line.view(dtype=complex).reshape(len(line)) if point_stack.ends_with(line[0]): diff --git a/vpype/metadata.py b/vpype/metadata.py index 033909ff..6b322f8c 100644 --- a/vpype/metadata.py +++ b/vpype/metadata.py @@ -39,7 +39,7 @@ def __init__( alpha: int | None = None, ): svgc = None - if isinstance(red, (svgelements.Color, Color)): + if isinstance(red, svgelements.Color | Color): svgc = red elif isinstance(red, str): svgc = svgelements.Color(red) diff --git a/vpype/model.py b/vpype/model.py index 1ef4a970..65bad0ae 100644 --- a/vpype/model.py +++ b/vpype/model.py @@ -4,8 +4,8 @@ import math import pathlib -from collections.abc import Iterable, Iterator -from typing import Any, Callable, Optional, Union, cast +from collections.abc import Callable, Iterable, Iterator +from typing import Any, Union, cast import numpy as np from shapely.geometry import LinearRing, LineString, MultiLineString @@ -28,7 +28,7 @@ "_MetadataMixin", # for documentation ] -LineLike = Union[LineString, LinearRing, Iterable[complex]] +LineLike = Union[LineString, LinearRing, Iterable[complex]] # noqa: UP007 # We accept LineString and LinearRing as line collection because MultiLineString are regularly # converted to LineString/LinearRing when operation reduce them to single-line construct. @@ -193,7 +193,7 @@ def append(self, line: LineLike) -> None: Args: line (LineLike): line to append """ - if isinstance(line, (LineString, LinearRing)): + if isinstance(line, LineString | LinearRing): # noinspection PyTypeChecker self._lines.append(np.array(line.coords).view(dtype=complex).reshape(-1)) else: @@ -222,7 +222,7 @@ def extend(self, lines: LineCollectionLike) -> None: # handle shapely objects if isinstance(lines, MultiLineString): lines = lines.geoms - elif isinstance(lines, (LineString, LinearRing)): + elif isinstance(lines, LineString | LinearRing): lines = [lines] for line in lines: @@ -623,7 +623,7 @@ def page_size(self) -> tuple[float, float] | None: return self.property(METADATA_FIELD_PAGE_SIZE) @page_size.setter - def page_size(self, page_size=Optional[tuple[float, float]]) -> None: + def page_size(self, page_size=tuple[float, float] | None) -> None: """Sets the page size to a new value.""" self.set_property(METADATA_FIELD_PAGE_SIZE, page_size) diff --git a/vpype/text.py b/vpype/text.py index 9df3e059..8aececa1 100644 --- a/vpype/text.py +++ b/vpype/text.py @@ -23,9 +23,9 @@ import itertools import pickle +from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Callable import pyphen diff --git a/vpype/utils.py b/vpype/utils.py index afedbf08..8c2464c7 100644 --- a/vpype/utils.py +++ b/vpype/utils.py @@ -2,7 +2,7 @@ import math import re -from typing import Callable +from collections.abc import Callable import numpy as np @@ -74,7 +74,7 @@ def _mm_to_px(x: float, y: float) -> tuple[float, float]: def _convert_unit(value: str | float | int, units: dict[str, float]) -> float: """Converts a string with unit to a value""" - if isinstance(value, (float, int)): + if isinstance(value, float | int): return value mo = _FLOAT_WITH_UNIT_RE.match(value.strip().lower()) diff --git a/vpype_cli/cli.py b/vpype_cli/cli.py index 067ee06f..ef1ef9de 100644 --- a/vpype_cli/cli.py +++ b/vpype_cli/cli.py @@ -8,8 +8,8 @@ import shlex import sys import traceback -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Callable, TextIO, Union, cast +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Any, TextIO, Union, cast import click import numpy as np @@ -204,11 +204,7 @@ def cli( # since python 3.10, a new "selectable" API is used for entry points, see: # https://docs.python.org/3/library/importlib.metadata.html#entry-points - # Not using it yields a deprecation warning in some circumstances. - if sys.version_info >= (3, 10): - entry_points = importlib.metadata.entry_points().select(group="vpype.plugins") - else: # pragma: no cover - entry_points = importlib.metadata.entry_points().get("vpype.plugins", []) + entry_points = importlib.metadata.entry_points().select(group="vpype.plugins") for entry_point in entry_points: # noinspection PyBroadException try: diff --git a/vpype_cli/metadata.py b/vpype_cli/metadata.py index bf4c72dd..87e394f6 100644 --- a/vpype_cli/metadata.py +++ b/vpype_cli/metadata.py @@ -1,7 +1,8 @@ from __future__ import annotations import logging -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import click diff --git a/vpype_cli/substitution.py b/vpype_cli/substitution.py index cb638c1f..c461b21b 100644 --- a/vpype_cli/substitution.py +++ b/vpype_cli/substitution.py @@ -4,8 +4,8 @@ import os import pathlib import sys -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Any import asteval diff --git a/vpype_viewer/_painters.py b/vpype_viewer/_painters.py index fa4d8877..5b47fe13 100644 --- a/vpype_viewer/_painters.py +++ b/vpype_viewer/_painters.py @@ -1,7 +1,7 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, cast import moderngl as mgl import numpy as np @@ -14,7 +14,7 @@ if TYPE_CHECKING: # pragma: no cover from .engine import Engine -ResourceType = Union[mgl.Buffer, mgl.Texture, mgl.TextureArray] +ResourceType = mgl.Buffer | mgl.Texture | mgl.TextureArray class Painter: diff --git a/vpype_viewer/engine.py b/vpype_viewer/engine.py index 828caa42..3e62963e 100644 --- a/vpype_viewer/engine.py +++ b/vpype_viewer/engine.py @@ -4,7 +4,7 @@ import enum from collections import defaultdict -from typing import Callable +from collections.abc import Callable import moderngl as mgl import numpy as np diff --git a/vpype_viewer/qtviewer/utils.py b/vpype_viewer/qtviewer/utils.py index a1a3e74c..3cbda5e4 100644 --- a/vpype_viewer/qtviewer/utils.py +++ b/vpype_viewer/qtviewer/utils.py @@ -3,8 +3,8 @@ import os import signal import socket +from collections.abc import Callable from contextlib import contextmanager -from typing import Callable from PySide6 import QtNetwork from PySide6.QtGui import QAction, QActionGroup, QGuiApplication, QIcon, QPalette