Skip to content

Commit 5f14fe4

Browse files
authored
Modernize project (#739)
Modernize project to latest practices. * Use `pyproject.toml` instead of `setup.py`. * Use hatchling instead of buildout to build project. * Use pytest instead of unittests and zope runner (decreasing dependencies). * Make `uv` the recommended tool for local development. * Simplify versioning when creating a new release. * Increase the number of tests and overall coverage (~95%).
1 parent c33bdc9 commit 5f14fe4

File tree

17 files changed

+2126
-1073
lines changed

17 files changed

+2126
-1073
lines changed

.github/workflows/tests.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,32 +49,32 @@ jobs:
4949
- name: Set up uv
5050
uses: astral-sh/setup-uv@v6
5151
with:
52-
cache-dependency-glob: |
53-
setup.py
5452
cache-suffix: ${{ matrix.python-version }}
5553
enable-cache: true
5654
version: "latest"
57-
55+
- name: Setup env
56+
run: uv sync
5857
- name: Invoke tests
5958
run: |
60-
59+
6160
# Propagate build matrix information.
6261
./devtools/setup_ci.sh
6362
6463
# Bootstrap environment.
6564
source bootstrap.sh
66-
65+
66+
# Run linter.
67+
uv run ruff check .
68+
69+
# Run type testing
70+
uv run mypy
71+
6772
# Report about the test matrix slot.
6873
echo "Invoking tests with CrateDB ${CRATEDB_VERSION}"
69-
70-
# Run linter.
71-
poe lint
74+
uv run coverage run -m pytest
7275
73-
# Run tests.
74-
coverage run bin/test -vvv
75-
7676
# Set the stage for uploading the coverage report.
77-
coverage xml
77+
uv run coverage xml
7878
7979
# https://github.com/codecov/codecov-action
8080
- name: Upload coverage results to Codecov

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Changes for crate
44

55
Unreleased
66
==========
7+
- Modernize project to latest practices:
8+
* Use `pyproject.toml` instead of `setup.py`.
9+
* Use hatchling instead of buildout to build project.
10+
* Use pytest instead of unittests and zope runner (decreasing dependencies).
11+
* Make `uv` the recommended tool for local development.
12+
* Simplify versioning when creating a new release.
13+
* Increase the number of tests and overall coverage.
714

815
2025/01/30 2.0.0
916
================

pyproject.toml

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
1+
[build-system]
2+
requires = ["hatchling >= 1.26"]
3+
build-backend = "hatchling.build"
4+
5+
[tool.hatch.build.targets.wheel]
6+
packages = ["src/crate"]
7+
8+
[tool.hatch.version]
9+
path = "src/crate/client/__init__.py"
10+
11+
[project]
12+
name = "crate-python"
13+
dynamic = ["version"]
14+
description = "CrateDB Python Client"
15+
authors = [{ name = "Crate.io", email = "office@crate.io" }]
16+
requires-python = ">=3.10"
17+
readme = "README.rst"
18+
license = { file = "LICENSE"}
19+
classifiers = [
20+
"Development Status :: 5 - Production/Stable",
21+
"Intended Audience :: Developers",
22+
"License :: OSI Approved :: Apache Software License",
23+
"Operating System :: OS Independent",
24+
"Programming Language :: Python",
25+
"Programming Language :: Python :: 3",
26+
"Programming Language :: Python :: 3.10",
27+
"Programming Language :: Python :: 3.11",
28+
"Programming Language :: Python :: 3.12",
29+
"Programming Language :: Python :: 3.13",
30+
"Programming Language :: Python :: Implementation :: CPython",
31+
"Programming Language :: Python :: Implementation :: PyPy",
32+
"Topic :: Database",
33+
]
34+
dependencies = [
35+
"orjson>=3.11.3",
36+
"urllib3>=2.5.0",
37+
]
38+
39+
[dependency-groups]
40+
dev = [
41+
"certifi>=2025.10.5",
42+
"coverage>=7.11.0",
43+
"mypy>=1.18.2",
44+
"pytest>=8.4.2",
45+
"pytz>=2025.2",
46+
"ruff>=0.14.2",
47+
"setuptools>=80.9.0",
48+
"stopit>=1.1.2",
49+
"verlib2>=0.3.1",
50+
]
51+
52+
153
[tool.mypy]
254
mypy_path = "src"
355
packages = [
@@ -18,65 +70,67 @@ non_interactive = true
1870
line-length = 80
1971

2072
extend-exclude = [
21-
"/example_*",
73+
"/example_*",
2274
]
2375

2476
lint.select = [
25-
# Builtins
26-
"A",
27-
# Bugbear
28-
"B",
29-
# comprehensions
30-
"C4",
31-
# Pycodestyle
32-
"E",
33-
# eradicate
34-
"ERA",
35-
# Pyflakes
36-
"F",
37-
# isort
38-
"I",
39-
# pandas-vet
40-
"PD",
41-
# return
42-
"RET",
43-
# Bandit
44-
"S",
45-
# print
46-
"T20",
47-
"W",
48-
# flake8-2020
49-
"YTT",
77+
# Builtins
78+
"A",
79+
# Bugbear
80+
"B",
81+
# comprehensions
82+
"C4",
83+
# Pycodestyle
84+
"E",
85+
# eradicate
86+
"ERA",
87+
# Pyflakes
88+
"F",
89+
# isort
90+
"I",
91+
# pandas-vet
92+
"PD",
93+
# return
94+
"RET",
95+
# Bandit
96+
"S",
97+
# print
98+
"T20",
99+
"W",
100+
# flake8-2020
101+
"YTT",
50102
]
51103

52104
lint.extend-ignore = [
53-
# Unnecessary variable assignment before `return` statement
54-
"RET504",
55-
# Unnecessary `elif` after `return` statement
56-
"RET505",
105+
# Unnecessary variable assignment before `return` statement
106+
"RET504",
107+
# Unnecessary `elif` after `return` statement
108+
"RET505",
57109
]
58110

59111
lint.per-file-ignores."example_*" = [
60-
"ERA001", # Found commented-out code
61-
"T201", # Allow `print`
112+
"ERA001", # Found commented-out code
113+
"T201", # Allow `print`
62114
]
63115
lint.per-file-ignores."devtools/*" = [
64-
"T201", # Allow `print`
116+
"T201", # Allow `print`
65117
]
66118
lint.per-file-ignores."examples/*" = [
67-
"ERA001", # Found commented-out code
68-
"T201", # Allow `print`
119+
"ERA001", # Found commented-out code
120+
"T201", # Allow `print`
69121
]
70122
lint.per-file-ignores."tests/*" = [
71-
"S106", # Possible hardcoded password assigned to argument: "password"
72-
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
123+
"S101", # Asserts.
124+
"S105", # Possible hardcoded password assigned to: "password"
125+
"S106", # Possible hardcoded password assigned to argument: "password"
126+
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
73127
]
74128
lint.per-file-ignores."src/crate/client/{connection.py,http.py}" = [
75-
"A004", # Import `ConnectionError` is shadowing a Python builtin
76-
"A005", # Import `ConnectionError` is shadowing a Python builtin
129+
"A004", # Import `ConnectionError` is shadowing a Python builtin
130+
"A005", # Import `ConnectionError` is shadowing a Python builtin
77131
]
78132
lint.per-file-ignores."tests/client/test_http.py" = [
79-
"A004", # Import `ConnectionError` is shadowing a Python builtin
133+
"A004", # Import `ConnectionError` is shadowing a Python builtin
80134
]
81135

82136

src/crate/client/blob.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def put(self, f, digest=None):
5656
read.
5757
:return:
5858
The hex digest of the uploaded blob if not provided in the call.
59-
Otherwise a boolean indicating if the blob has been newly created.
59+
Otherwise, a boolean indicating if the blob has been newly created.
6060
"""
6161

6262
if digest:

src/crate/client/connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def _lowest_server_version(self):
208208
return lowest or Version("0.0.0")
209209

210210
def __repr__(self):
211-
return "<Connection {0}>".format(repr(self.client))
211+
return f"<{self.__class__.__qualname__} {self.client!r}>"
212212

213213
def __enter__(self):
214214
return self

src/crate/client/cursor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ def _convert_rows(self):
236236

237237
# Process result rows with conversion.
238238
for row in self._result["rows"]:
239-
yield [convert(value) for convert, value in zip(converters, row)]
239+
yield [
240+
convert(value)
241+
for convert, value in zip(converters, row, strict=False)
242+
]
240243

241244
@property
242245
def time_zone(self):

src/crate/client/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def __init__(self, table, digest):
8686
self.digest = digest
8787

8888
def __str__(self):
89-
return "{table}/{digest}".format(table=self.table, digest=self.digest)
89+
return f"{self.__class__.__qualname__}('{self.table}/{self.digest})'"
9090

9191

9292
class DigestNotFoundException(BlobException):

src/crate/client/http.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,10 @@ def _pool_kw_args(
326326
return kw
327327

328328

329-
def _remove_certs_for_non_https(server, kwargs):
329+
def _remove_certs_for_non_https(server: str, kwargs: dict) -> dict:
330+
"""
331+
Removes certificates for http requests.
332+
"""
330333
if server.lower().startswith("https"):
331334
return kwargs
332335
used_ssl_args = SSL_ONLY_ARGS & set(kwargs.keys())
@@ -435,6 +438,7 @@ def __init__(
435438
if servers and not username:
436439
try:
437440
url = urlparse(servers[0])
441+
438442
if url.username is not None:
439443
username = url.username
440444
if url.password is not None:

tests/client/test_blob.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from io import BytesIO
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
6+
from crate.client.blob import BlobContainer
7+
8+
9+
def test_container():
10+
"""Verify a container can be instantiated."""
11+
expected_name = "somename"
12+
container = BlobContainer(expected_name, MagicMock())
13+
assert container.container_name == expected_name
14+
15+
16+
def test_container_digest():
17+
digester = BlobContainer("", MagicMock())._compute_digest
18+
19+
# sha1 of some_data.
20+
some_data, expected_digest = (
21+
b"some_data_123456",
22+
"51bea75c0f26998083ef3717a489f2dc05818e8d",
23+
)
24+
result = digester(BytesIO(some_data))
25+
assert result == expected_digest
26+
27+
with pytest.raises(AttributeError):
28+
digester("someundigestabledata")
29+
30+
31+
def test_container_put():
32+
"""Test the logic of container put method"""
33+
some_data, expected_digest = (
34+
b"some_data_123456",
35+
"51bea75c0f26998083ef3717a489f2dc05818e8d",
36+
)
37+
expected_container_name = "somename"
38+
m = MagicMock()
39+
m.client.blob_put = MagicMock()
40+
container = BlobContainer(expected_container_name, m)
41+
42+
result = container.put(BytesIO(some_data))
43+
assert result == expected_digest
44+
45+
new_digest = "asdfn"
46+
data = BytesIO(some_data)
47+
result = container.put(data, digest=new_digest)
48+
assert isinstance(result, MagicMock)
49+
assert m.client.blob_put.call_count == 2
50+
assert m.client.blob_put.call_args.args == (
51+
expected_container_name,
52+
new_digest,
53+
data,
54+
)

0 commit comments

Comments
 (0)