Skip to content

Commit cbf68e1

Browse files
Merge branch 'main' into cerberus-to-pydantic
2 parents 9e33ad9 + 4c265e8 commit cbf68e1

File tree

97 files changed

+5496
-5737
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+5496
-5737
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
run: uv sync --group dev
7272

7373
- name: Run unit tests
74-
run: uv run python -m unittest discover -s tests
74+
run: uv run pytest
7575
env:
7676
PYTHONPATH: ./src
7777
SURREALDB_URL: http://localhost:8000
@@ -117,7 +117,7 @@ jobs:
117117
run: uv sync --group dev
118118

119119
- name: Run unit tests
120-
run: uv run python -m unittest discover -s tests
120+
run: uv run pytest
121121
env:
122122
PYTHONPATH: ./src
123123
SURREALDB_URL: http://localhost:8000

README.md

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,10 @@ This project follows library best practices for dependency management:
7777
# Run type checking
7878
uv run mypy --explicit-package-bases src/
7979

80-
# Run tests
81-
uv run python -m unittest discover -s tests
82-
83-
# Run tests with pytest (recommended)
84-
uv run pytest
85-
86-
# Run tests with coverage
87-
./scripts/run_tests_with_coverage.sh
80+
# Run tests (with coverage)
81+
uv run scripts/run_tests.sh
82+
# Or directly:
83+
uv run pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
8884
```
8985

9086
3. **Build the project:**
@@ -101,17 +97,14 @@ We use a multi-tier testing strategy to ensure compatibility across SurrealDB ve
10197
```bash
10298
# Test with default version (latest stable)
10399
docker-compose up -d
104-
uv run pytest
105-
106-
# Test with coverage
107-
./scripts/run_tests_with_coverage.sh
100+
uv run scripts/run_tests.sh
108101

109102
# Test against specific version
110103
./scripts/test-versions.sh v2.1.8
111104

112105
# Test against different v2.x versions
113-
SURREALDB_VERSION=v2.0.5 uv run pytest
114-
SURREALDB_VERSION=v2.3.6 uv run pytest
106+
SURREALDB_VERSION=v2.0.5 uv run scripts/run_tests.sh
107+
SURREALDB_VERSION=v2.3.6 uv run scripts/run_tests.sh
115108
```
116109

117110
### CI/CD Testing
@@ -248,18 +241,26 @@ bash scripts/term.sh
248241
You will now be running an interactive terminal through a python virtual environment with all the dependencies installed. We can now run the tests with the following command:
249242

250243
```bash
251-
python -m unittest discover
244+
pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
252245
```
253246

254247
The number of tests might increase but at the time of writing this you should get a printout like the one below:
255248

256249
```bash
257-
.........................................................................................................................................Error in live subscription: sent 1000 (OK); no close frame received
258-
..........................................................................................
259-
----------------------------------------------------------------------
260-
Ran 227 tests in 6.313s
250+
================================ test session starts ================================
251+
platform ...
252+
collected 227 items
261253

262-
OK
254+
....................................................................................
255+
... (test output)
256+
257+
---------- coverage: platform ... -----------
258+
Name Stmts Miss Cover Missing
259+
---------------------------------------------------------
260+
src/surrealdb/....
261+
...
262+
263+
============================= 227 passed in 6.31s ================================
263264
```
264265
Finally, we clean up the database with the command below:
265266
```bash
@@ -283,11 +284,11 @@ Test against different SurrealDB versions using environment variables:
283284

284285
```bash
285286
# Test with latest v2.x (default: v2.3.6)
286-
uv run python -m unittest discover -s tests
287+
uv run scripts/run_tests.sh
287288

288289
# Test with specific v2.x version
289290
SURREALDB_VERSION=v2.1.8 docker-compose up -d surrealdb
290-
uv run python -m unittest discover -s tests
291+
uv run scripts/run_tests.sh
291292

292293
# Use different profiles for testing specific v2.x versions
293294
docker-compose --profile v2-0 up -d # v2.0.5 on port 8020
@@ -507,3 +508,30 @@ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guid
507508
## License
508509

509510
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
511+
512+
# Running Tests and Coverage
513+
514+
To run all tests with coverage reporting:
515+
516+
```bash
517+
uv run scripts/run_tests.sh
518+
```
519+
520+
This will:
521+
- Run all tests using pytest
522+
- Show a coverage summary in the terminal
523+
- Generate an HTML coverage report in the `htmlcov/` directory
524+
525+
You can also run tests directly with:
526+
527+
```bash
528+
uv run pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
529+
```
530+
531+
To test a specific file:
532+
533+
```bash
534+
uv run pytest tests/unit_tests/connections/test_connection_constructor.py --cov=src/surrealdb
535+
```
536+
537+
To view the HTML coverage report, open `htmlcov/index.html` in your browser after running the tests.

pyproject.toml

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "surrealdb"
7-
version = "1.0.4"
7+
version = "1.0.6"
88
description = "SurrealDB python client"
99
readme = "README.md"
1010
authors = [{ name = "SurrealDB" }]
@@ -44,26 +44,27 @@ documentation = "https://surrealdb.com/docs/sdk/python"
4444
packages = ["src/surrealdb"]
4545

4646
[tool.hatch.build.targets.sdist]
47-
include = [
48-
"/src",
49-
"/tests",
50-
"/README.md",
51-
"/LICENSE",
52-
]
47+
include = ["/src", "/tests", "/README.md", "/LICENSE"]
5348

5449
[tool.ruff]
5550
exclude = ["src/surrealdb/__init__.py"]
5651

5752
[tool.ruff.lint]
5853
select = [
59-
"I", # isort
60-
"UP", # pyupgrade
54+
"I", # isort
55+
"UP", # pyupgrade
6156
]
6257

6358
[tool.mypy]
6459
mypy_path = "src"
6560
explicit_package_bases = true
66-
disable_error_code = ["return-value", "var-annotated", "assignment", "arg-type", "attr-defined"]
61+
disable_error_code = [
62+
"return-value",
63+
"var-annotated",
64+
"assignment",
65+
"arg-type",
66+
"attr-defined",
67+
]
6768

6869
[[tool.mypy.overrides]]
6970
module = "aiohttp.*"
@@ -78,27 +79,50 @@ testpaths = ["tests"]
7879
python_files = ["test_*.py"]
7980
python_classes = ["Test*"]
8081
python_functions = ["test_*"]
82+
asyncio_mode = "auto"
8183
addopts = [
82-
"-v",
83-
"--tb=short",
8484
"--strict-markers",
85-
"--disable-warnings",
85+
"--strict-config",
86+
"--verbose",
87+
"--tb=short",
8688
]
87-
markers = [
88-
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
89-
"integration: marks tests as integration tests",
90-
"unit: marks tests as unit tests",
89+
filterwarnings = [
90+
"ignore::pytest.PytestUnraisableExceptionWarning",
91+
]
92+
93+
[tool.coverage.run]
94+
source = ["src"]
95+
omit = [
96+
"*/tests/*",
97+
"*/test_*",
98+
"*/__pycache__/*",
99+
]
100+
101+
[tool.coverage.report]
102+
exclude_lines = [
103+
"pragma: no cover",
104+
"def __repr__",
105+
"if self.debug:",
106+
"if settings.DEBUG",
107+
"raise AssertionError",
108+
"raise NotImplementedError",
109+
"if 0:",
110+
"if __name__ == .__main__.:",
111+
"class .*\\bProtocol\\):",
112+
"@(abc\\.)?abstractmethod",
91113
]
92114

93115
[dependency-groups]
94116
dev = [
95117
{ include-group = "test" },
96118
"mypy>=1.0.0",
97119
"ruff>=0.12.0",
98-
"types-requests>=2.25.0", # Type stubs for requests
120+
"types-requests>=2.25.0", # Type stubs for requests
99121
]
100122
test = [
123+
"coverage>=7.0.0",
101124
"hypothesis>=6.135.16",
102125
"pytest>=7.0.0",
126+
"pytest-asyncio>=0.21.0",
103127
"pytest-cov>=4.0.0",
104128
]

scripts/run_tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ cd ..
88
cd src
99
export PYTHONPATH=$(pwd)
1010
cd ..
11-
python -m unittest discover
11+
12+
# Run tests with coverage
13+
pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html

src/surrealdb/connections/async_ws.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from uuid import UUID
1010

1111
import websockets # type: ignore
12+
from websockets.exceptions import ConnectionClosed, WebSocketException
1213

1314
from surrealdb.connections.async_template import AsyncTemplate
1415
from surrealdb.connections.url import Url
@@ -50,17 +51,33 @@ def __init__(
5051

5152
async def _recv_task(self):
5253
assert self.socket
53-
async for data in self.socket:
54-
response = decode(data)
55-
if response_id := response.get("id"):
56-
if fut := self.qry.get(response_id):
57-
fut.set_result(response)
58-
elif response_result := response.get("result"):
59-
live_id = str(response_result["id"])
60-
for queue in self.live_queues.get(live_id, []):
61-
queue.put_nowait(response_result)
62-
else:
63-
self.check_response_for_error(response, "_recv_task")
54+
try:
55+
async for data in self.socket:
56+
response = decode(data)
57+
if response_id := response.get("id"):
58+
if fut := self.qry.get(response_id):
59+
fut.set_result(response)
60+
elif response_result := response.get("result"):
61+
live_id = str(response_result["id"])
62+
for queue in self.live_queues.get(live_id, []):
63+
queue.put_nowait(response_result)
64+
else:
65+
self.check_response_for_error(response, "_recv_task")
66+
except (ConnectionClosed, WebSocketException, asyncio.CancelledError):
67+
# Connection was closed or cancelled, this is expected
68+
pass
69+
except Exception as e:
70+
# Log unexpected errors but don't let them propagate
71+
import logging
72+
73+
logger = logging.getLogger(__name__)
74+
logger.debug(f"Unexpected error in _recv_task: {e}")
75+
finally:
76+
# Clean up any pending futures
77+
for fut in self.qry.values():
78+
if not fut.done():
79+
fut.cancel()
80+
self.qry.clear()
6481

6582
async def _send(
6683
self, message: RequestMessage, process: str, bypass: bool = False
@@ -337,15 +354,27 @@ async def upsert(
337354
return response["result"]
338355

339356
async def close(self):
340-
if self.recv_task:
357+
# Cancel the receive task first
358+
if self.recv_task and not self.recv_task.done():
341359
self.recv_task.cancel()
342360
try:
343361
await self.recv_task
344362
except asyncio.CancelledError:
345363
pass
364+
except Exception:
365+
# Ignore any other exceptions during cleanup
366+
pass
346367

368+
# Close the WebSocket connection
347369
if self.socket is not None:
348-
await self.socket.close()
370+
try:
371+
await self.socket.close()
372+
except Exception:
373+
# Ignore exceptions during socket closure
374+
pass
375+
finally:
376+
self.socket = None
377+
self.recv_task = None
349378

350379
async def __aenter__(self) -> "AsyncWsSurrealConnection":
351380
"""

src/surrealdb/data/types/duration.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ def parse(value: Union[str, int], nanoseconds: int = 0) -> "Duration":
2323
if isinstance(value, int):
2424
return Duration(nanoseconds + value * UNITS["s"])
2525
elif isinstance(value, str):
26+
# Check for multi-character units first
27+
for unit in ["ns", "us", "ms"]:
28+
if value.endswith(unit):
29+
num = int(value[: -len(unit)])
30+
return Duration(num * UNITS[unit])
31+
# Check for single-character units
2632
unit = value[-1]
2733
num = int(value[:-1])
2834
if unit in UNITS:
@@ -75,7 +81,7 @@ def weeks(self) -> int:
7581
return self.elapsed // UNITS["w"]
7682

7783
def to_string(self) -> str:
78-
for unit in reversed(["w", "d", "h", "m", "s", "ms", "us", "ns"]):
84+
for unit in ["w", "d", "h", "m", "s", "ms", "us", "ns"]:
7985
value = self.elapsed // UNITS[unit]
8086
if value > 0:
8187
return f"{value}{unit}"
Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,7 @@
1-
from unittest import IsolatedAsyncioTestCase, main
1+
import pytest
22

33
from surrealdb.connections.async_ws import AsyncWsSurrealConnection
44

55

6-
class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase):
7-
async def asyncSetUp(self):
8-
self.url = "ws://localhost:8000"
9-
self.password = "root"
10-
self.username = "root"
11-
self.vars_params = {
12-
"username": self.username,
13-
"password": self.password,
14-
}
15-
self.database_name = "test_db"
16-
self.namespace = "test_ns"
17-
self.connection = AsyncWsSurrealConnection(self.url)
18-
_ = await self.connection.signin(self.vars_params)
19-
_ = await self.connection.use(
20-
namespace=self.namespace, database=self.database_name
21-
)
22-
23-
async def test_authenticate(self):
24-
outcome = await self.connection.authenticate(token=self.connection.token)
25-
26-
27-
if __name__ == "__main__":
28-
main()
6+
async def test_authenticate(async_ws_connection):
7+
outcome = await async_ws_connection.authenticate(token=async_ws_connection.token)

0 commit comments

Comments
 (0)