Skip to content

Commit cdb9b68

Browse files
Add support for Python 3.10 (#246)
* Add support for Python 3.10 * Fix CI * Fix CI * Pass tests
1 parent 014f9ad commit cdb9b68

38 files changed

+342
-140
lines changed

.github/workflows/ci.yml

+14-4
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,23 @@ jobs:
4242

4343
- name: Run benchmarks
4444
uses: CodSpeedHQ/action@513a19673a831f139e8717bf45ead67e47f00044 # v3.2.0
45-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.resolution == 'highest'
45+
if: matrix.codspeed
4646
with:
4747
token: ${{ secrets.CODSPEED_TOKEN }}
4848
run: pytest --codspeed
4949

5050
strategy:
5151
matrix:
52-
os: [ubuntu-latest, macos-latest, windows-latest]
53-
python-version: [3.11, 3.12, 3.13]
54-
resolution: [highest, lowest-direct]
52+
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
53+
python-version: ["3.10", "3.11", "3.12", "3.13"]
54+
resolution: ["highest"]
55+
codspeed: [false]
56+
include:
57+
- os: "ubuntu-latest"
58+
python-version: "3.10"
59+
resolution: "lowest-direct"
60+
codspeed: false
61+
- os: "ubuntu-latest"
62+
python-version: "3.13"
63+
resolution: "highest"
64+
codspeed: true

.python-version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.11.11
1+
3.10.14

pyproject.toml

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ license = { file = "LICENSE" }
1212
authors = [
1313
{ name = "Nathan McDougall", email = "nathan.j.mcdougall@gmail.com" },
1414
]
15-
requires-python = ">=3.11"
15+
requires-python = ">=3.10"
1616
classifiers = [
1717
"Development Status :: 2 - Pre-Alpha",
1818
"Environment :: Console",
1919
"Intended Audience :: Developers",
2020
"License :: OSI Approved :: MIT License",
2121
"Operating System :: OS Independent",
2222
"Programming Language :: Python :: 3 :: Only",
23+
"Programming Language :: Python :: 3.10",
2324
"Programming Language :: Python :: 3.11",
2425
"Programming Language :: Python :: 3.12",
2526
"Programming Language :: Python :: 3.13",
@@ -41,17 +42,18 @@ dependencies = [
4142
"ruamel-yaml>=0.18.6",
4243
"tomlkit>=0.13.2",
4344
"typer>=0.12.5",
45+
"typing-extensions>=4.12.2",
4446
]
4547
scripts.usethis = "usethis.__main__:app"
4648

4749
[dependency-groups]
4850
dev = [
4951
"datamodel-code-generator[http]>=0.26.2",
50-
"deptry>=0.20.0",
52+
"deptry>=0.23.0",
5153
"import-linter>=2.1",
5254
"pre-commit>=4.0.1",
5355
"pyright>=1.1.391",
54-
"ruff>=0.7.1",
56+
"ruff>=0.9.4",
5557
]
5658
test = [
5759
"coverage[toml]>=7.6.3",

src/usethis/_core/badge.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import re
22
import sys
33
from pathlib import Path
4-
from typing import Self
54

65
from pydantic import BaseModel
6+
from typing_extensions import Self
77

88
from usethis._console import err_print, tick_print, warn_print
99
from usethis._core.readme import add_readme, get_readme_path

src/usethis/_integrations/bitbucket/cache.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from usethis._console import tick_print
22
from usethis._integrations.bitbucket.dump import bitbucket_fancy_dump
3-
from usethis._integrations.bitbucket.io import (
3+
from usethis._integrations.bitbucket.io_ import (
44
BitbucketPipelinesYAMLDocument,
55
edit_bitbucket_pipelines_yaml,
66
)
@@ -43,7 +43,7 @@ def _add_caches_via_doc(
4343
for name, cache in cache_by_name.items():
4444
if not _cache_exists(name, doc=doc):
4545
tick_print(
46-
f"Adding cache '{name}' definition to " f"'bitbucket-pipelines.yml'."
46+
f"Adding cache '{name}' definition to 'bitbucket-pipelines.yml'."
4747
)
4848
config.definitions.caches[name] = cache
4949

src/usethis/_integrations/bitbucket/io.py renamed to src/usethis/_integrations/bitbucket/io_.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from usethis._console import tick_print
1010
from usethis._integrations.bitbucket.schema import PipelinesConfiguration
11-
from usethis._integrations.yaml.io import YAMLLiteral, edit_yaml
11+
from usethis._integrations.yaml.io_ import YAMLLiteral, edit_yaml
1212

1313

1414
class BitbucketPipelinesYAMLConfigError(Exception):
@@ -29,9 +29,9 @@ class BitbucketPipelinesYAMLDocument:
2929

3030

3131
@contextmanager
32-
def edit_bitbucket_pipelines_yaml() -> (
33-
Generator[BitbucketPipelinesYAMLDocument, None, None]
34-
):
32+
def edit_bitbucket_pipelines_yaml() -> Generator[
33+
BitbucketPipelinesYAMLDocument, None, None
34+
]:
3535
"""A context manager to modify 'bitbucket-pipelines.yml' in-place."""
3636
name = "bitbucket-pipelines.yml"
3737
path = Path.cwd() / name

src/usethis/_integrations/bitbucket/pipeweld.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from functools import singledispatch
2-
from typing import assert_never
32
from uuid import uuid4
43

4+
from typing_extensions import assert_never
5+
56
import usethis._pipeweld.containers
67
from usethis._integrations.bitbucket.dump import bitbucket_fancy_dump
78
from usethis._integrations.bitbucket.errors import UnexpectedImportPipelineError
8-
from usethis._integrations.bitbucket.io import (
9+
from usethis._integrations.bitbucket.io_ import (
910
BitbucketPipelinesYAMLDocument,
1011
edit_bitbucket_pipelines_yaml,
1112
)

src/usethis/_integrations/bitbucket/schema.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# filename: schema.json
33
# timestamp: 2025-01-13T20:41:41+00:00
44
# using the command:
5-
# datamodel-codegen --input tests\usethis\_integrations\bitbucket\schema.json --input-file-type jsonschema --output src\usethis\_integrations\bitbucket\schema.py --enum-field-as-literal all --field-constraints --use-double-quotes --use-union-operator --use-standard-collections --use-default-kwarg --output-model-type pydantic_v2.BaseModel --target-python-version 3.11
5+
# datamodel-codegen --input tests\usethis\_integrations\bitbucket\schema.json --input-file-type jsonschema --output src\usethis\_integrations\bitbucket\schema.py --enum-field-as-literal all --field-constraints --use-double-quotes --use-union-operator --use-standard-collections --use-default-kwarg --output-model-type pydantic_v2.BaseModel --target-python-version 3.10
66
# ruff: noqa: ERA001
77
# pyright: reportGeneralTypeIssues=false
88
# plus manually add Definitions.scripts for type hinting

src/usethis/_integrations/bitbucket/steps.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from functools import singledispatch
22
from pathlib import Path
3-
from typing import assert_never
43

54
from ruamel.yaml.anchor import Anchor
65
from ruamel.yaml.comments import CommentedSeq
76
from ruamel.yaml.scalarstring import LiteralScalarString
7+
from typing_extensions import assert_never
88

99
import usethis._pipeweld.func
1010
from usethis._console import box_print, tick_print
1111
from usethis._integrations.bitbucket.anchor import ScriptItemAnchor, ScriptItemName
1212
from usethis._integrations.bitbucket.cache import _add_caches_via_doc, remove_cache
1313
from usethis._integrations.bitbucket.dump import bitbucket_fancy_dump
1414
from usethis._integrations.bitbucket.errors import UnexpectedImportPipelineError
15-
from usethis._integrations.bitbucket.io import (
15+
from usethis._integrations.bitbucket.io_ import (
1616
BitbucketPipelinesYAMLDocument,
1717
edit_bitbucket_pipelines_yaml,
1818
)
@@ -101,8 +101,7 @@ def _add_step_in_default_via_doc(
101101
# It's not always notable that the placeholder is being added.
102102
else:
103103
tick_print(
104-
f"Adding '{step.name}' to default pipeline in "
105-
f"'bitbucket-pipelines.yml'."
104+
f"Adding '{step.name}' to default pipeline in 'bitbucket-pipelines.yml'."
106105
)
107106

108107
config = doc.model

src/usethis/_integrations/pre_commit/hooks.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from usethis._console import box_print, tick_print
55
from usethis._integrations.pre_commit.dump import pre_commit_fancy_dump
6-
from usethis._integrations.pre_commit.io import edit_pre_commit_config_yaml
6+
from usethis._integrations.pre_commit.io_ import edit_pre_commit_config_yaml
77
from usethis._integrations.pre_commit.schema import (
88
HookDefinition,
99
JsonSchemaForPreCommitConfigYaml,

src/usethis/_integrations/pre_commit/io.py renamed to src/usethis/_integrations/pre_commit/io_.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from usethis._console import tick_print
1010
from usethis._integrations.pre_commit.schema import JsonSchemaForPreCommitConfigYaml
11-
from usethis._integrations.yaml.io import YAMLLiteral, edit_yaml
11+
from usethis._integrations.yaml.io_ import YAMLLiteral, edit_yaml
1212

1313

1414
class PreCommitConfigYAMLConfigError(Exception):

src/usethis/_integrations/pre_commit/schema.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# filename: schema.json
33
# timestamp: 2024-12-13T02:06:43+00:00
44
# using the command:
5-
# datamodel-codegen --input tests\usethis\_integrations\pre_commit\schema.json --input-file-type jsonschema --output src\usethis\_integrations\pre_commit\schema.py --enum-field-as-literal all --field-constraints --use-double-quotes --use-union-operator --use-standard-collections --use-default-kwarg --output-model-type pydantic_v2.BaseModel --target-python-version 3.11
5+
# datamodel-codegen --input tests\usethis\_integrations\pre_commit\schema.json --input-file-type jsonschema --output src\usethis\_integrations\pre_commit\schema.py --enum-field-as-literal all --field-constraints --use-double-quotes --use-union-operator --use-standard-collections --use-default-kwarg --output-model-type pydantic_v2.BaseModel --target-python-version 3.10
66
# ruff: noqa: ERA001
77
# pyright: reportGeneralTypeIssues=false
88
# plus manually remove default for LocalRepo.repo

src/usethis/_integrations/pydantic/dump.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,39 @@ def _(
9999
return d
100100

101101

102-
@fancy_model_dump.register(bool | int | float | str)
102+
# N.B. when Python 3.10 support is dropped, we can register unions of types, rather than
103+
# having these separate identical implementations for each type.
104+
@fancy_model_dump.register(bool)
105+
def _(
106+
model: ModelLiteral,
107+
*,
108+
reference: ModelRepresentation | None = None,
109+
order_by_cls: dict[type[BaseModel], list[str]] | None = None,
110+
) -> ModelLiteral:
111+
return model
112+
113+
114+
@fancy_model_dump.register(int)
115+
def _(
116+
model: ModelLiteral,
117+
*,
118+
reference: ModelRepresentation | None = None,
119+
order_by_cls: dict[type[BaseModel], list[str]] | None = None,
120+
) -> ModelLiteral:
121+
return model
122+
123+
124+
@fancy_model_dump.register(float)
125+
def _(
126+
model: ModelLiteral,
127+
*,
128+
reference: ModelRepresentation | None = None,
129+
order_by_cls: dict[type[BaseModel], list[str]] | None = None,
130+
) -> ModelLiteral:
131+
return model
132+
133+
134+
@fancy_model_dump.register(str)
103135
def _(
104136
model: ModelLiteral,
105137
*,

src/usethis/_integrations/pyproject/core.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
PyProjectTOMLValueAlreadySetError,
99
PyProjectTOMLValueMissingError,
1010
)
11-
from usethis._integrations.pyproject.io import (
11+
from usethis._integrations.pyproject.io_ import (
1212
read_pyproject_toml,
1313
write_pyproject_toml,
1414
)

src/usethis/_integrations/pyproject/io.py renamed to src/usethis/_integrations/pyproject/io_.py

+4-16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import tomllib
21
from functools import cache
32
from pathlib import Path
4-
from typing import Any
53

64
import tomlkit
5+
from tomlkit.exceptions import TOMLKitError
76

87
from usethis._integrations.pyproject.errors import (
98
PyProjectTOMLDecodeError,
@@ -22,20 +21,9 @@ def read_pyproject_toml_from_path(path: Path) -> tomlkit.TOMLDocument:
2221
except FileNotFoundError:
2322
msg = "'pyproject.toml' not found in the current directory."
2423
raise PyProjectTOMLNotFoundError(msg)
25-
26-
27-
def read_pyproject_dict() -> dict[str, Any]:
28-
try:
29-
with Path("pyproject.toml").open("rb") as f:
30-
try:
31-
return tomllib.load(f)
32-
except tomllib.TOMLDecodeError as err:
33-
msg = f"Error decoding 'pyproject.toml': {err}"
34-
raise PyProjectTOMLDecodeError(msg)
35-
36-
except FileNotFoundError:
37-
msg = "'pyproject.toml' not found in the current directory."
38-
raise PyProjectTOMLNotFoundError(msg)
24+
except TOMLKitError as err:
25+
msg = f"Failed to decode 'pyproject.toml': {err}"
26+
raise PyProjectTOMLDecodeError(msg) from None
3927

4028

4129
def write_pyproject_toml(toml_document: tomlkit.TOMLDocument) -> None:

src/usethis/_integrations/pyproject/project.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from pydantic import TypeAdapter, ValidationError
44

55
from usethis._integrations.pyproject.errors import PyProjectTOMLProjectSectionError
6-
from usethis._integrations.pyproject.io import read_pyproject_dict
6+
from usethis._integrations.pyproject.io_ import read_pyproject_toml
77

88

99
def get_project_dict() -> dict[str, Any]:
10-
pyproject = read_pyproject_dict()
10+
pyproject = read_pyproject_toml().value
1111

1212
try:
1313
project = TypeAdapter(dict).validate_python(pyproject["project"])

src/usethis/_integrations/pyproject/requires_python.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from packaging.specifiers import SpecifierSet
22
from pydantic import TypeAdapter
33

4-
from usethis._integrations.pyproject.io import read_pyproject_toml
4+
from usethis._integrations.pyproject.io_ import read_pyproject_toml
55

66

77
class MissingRequiresPythonError(Exception):

src/usethis/_integrations/sonarqube/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_sonar_project_properties() -> str:
6868
sonar.sources=./src
6969
sonar.tests=./tests
7070
sonar.python.coverage.reportPaths={coverage_output}
71-
sonar.verbose={'true' if verbose else 'false'}
71+
sonar.verbose={"true" if verbose else "false"}
7272
"""
7373
if exclusions:
7474
text += "sonar.exclusions=" + ", ".join(exclusions) + "\n"

src/usethis/_integrations/uv/call.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from usethis._integrations.pyproject.io import read_pyproject_toml_from_path
1+
from usethis._integrations.pyproject.io_ import read_pyproject_toml_from_path
22
from usethis._integrations.uv.errors import UVSubprocessFailedError
33
from usethis._subprocess import SubprocessFailedError, call_subprocess
44

src/usethis/_integrations/uv/deps.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from usethis._config import usethis_config
55
from usethis._console import tick_print
6-
from usethis._integrations.pyproject.io import read_pyproject_toml
6+
from usethis._integrations.pyproject.io_ import read_pyproject_toml
77
from usethis._integrations.uv.call import call_uv_subprocess
88
from usethis._integrations.uv.errors import UVDepGroupError, UVSubprocessFailedError
99

src/usethis/_integrations/yaml/update.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
CommentedMap,
66
)
77

8-
from usethis._integrations.yaml.io import YAMLLiteral
8+
from usethis._integrations.yaml.io_ import YAMLLiteral
99

1010

1111
def update_ruamel_yaml_map(

src/usethis/_pipeweld/func.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import contextlib
22
from functools import reduce, singledispatch, singledispatchmethod
3-
from typing import assert_never
43

54
from pydantic import BaseModel
5+
from typing_extensions import assert_never
66

77
from usethis._pipeweld.containers import (
88
DepGroup,

tests/usethis/_core/test_ci.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,13 @@ def test_mentioned_in_file(
8080
assert "pre-commit" in contents
8181
out, err = capfd.readouterr()
8282
assert not err
83-
assert (
84-
out
85-
== (
86-
"✔ Writing 'bitbucket-pipelines.yml'.\n"
87-
"✔ Adding cache 'uv' definition to 'bitbucket-pipelines.yml'.\n"
88-
"✔ Adding cache 'pre-commit' definition to 'bitbucket-pipelines.yml'.\n"
89-
"✔ Adding 'Run pre-commit' to default pipeline in 'bitbucket-pipelines.yml'.\n"
90-
"ℹ Consider `usethis tool pytest` to test your code for the pipeline.\n" # noqa: RUF001
91-
"☐ Run your pipeline via the Bitbucket website.\n"
92-
)
83+
assert out == (
84+
"✔ Writing 'bitbucket-pipelines.yml'.\n"
85+
"✔ Adding cache 'uv' definition to 'bitbucket-pipelines.yml'.\n"
86+
"✔ Adding cache 'pre-commit' definition to 'bitbucket-pipelines.yml'.\n"
87+
"✔ Adding 'Run pre-commit' to default pipeline in 'bitbucket-pipelines.yml'.\n"
88+
"ℹ Consider `usethis tool pytest` to test your code for the pipeline.\n" # noqa: RUF001
89+
"☐ Run your pipeline via the Bitbucket website.\n"
9390
)
9491

9592
def test_not_mentioned_if_not_used(self, uv_init_dir: Path):

0 commit comments

Comments
 (0)