Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:
- name: Install dependencies
run: uv sync

- name: Type check with ty
run: uv run ty check

- name: Check line length violations with ruff
run: uv run ruff check --select=E501 --exit-zero

Expand Down
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["astral-sh.ty", "charliermarsh.ruff"]
}
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"python.testing.unittestEnabled": false,
"python.languageServer": "None",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"]
}
7 changes: 7 additions & 0 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"languages": {
"Python": {
"language_servers": ["ty", "ruff"]
}
}
}
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import tomllib
import tomllib # ty: ignore[unresolved-import]

# Configuration file for the Sphinx documentation builder.
#
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ sphinx==9.1.0 ; python_full_version >= '3.12'
# python-docs-theme
# sphinx-autoapi
# sphinx-autobuild
sphinx-autoapi==3.8.0 ; python_full_version >= '3.12'
sphinx-autoapi==3.8.1 ; python_full_version >= '3.12'
sphinx-autobuild==2025.8.25 ; python_full_version >= '3.12'
sphinxcontrib-applehelp==2.0.0 ; python_full_version >= '3.12'
# via sphinx
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Repository = "https://github.com/iskandergaba/free-threading"

[dependency-groups]
dev = [
"ty~=0.0.70",
"ruff~=0.16.0",
"pytest~=9.1.0",
"pytest-cov~=7.1.0",
Expand Down
47 changes: 19 additions & 28 deletions src/freethreading/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,24 @@ def _get_mp_context():
from threading import Semaphore as _Semaphore
from threading import Thread as _Worker
from threading import active_count as _active_count
from threading import current_thread as _current_worker
from threading import current_thread
from threading import enumerate as _enumerate
from threading import get_ident as _get_ident

_current_worker = current_thread

def _active_children():
children = list(_enumerate())
children.remove(_current_worker())
children.remove(current_thread())
return children

else:
from concurrent.futures import ProcessPoolExecutor as _WorkerPoolExecutor
from multiprocessing import active_children as _active_children
from multiprocessing import current_process as _current_worker
from multiprocessing import current_process
from os import getpid as _get_ident

_current_worker = current_process
_Barrier = _get_mp_context().Barrier
_BoundedSemaphore = _get_mp_context().BoundedSemaphore
_Condition = _get_mp_context().Condition
Expand All @@ -159,7 +162,7 @@ def _active_count():

def _enumerate():
workers = list(_active_children())
workers.append(_current_worker())
workers.append(current_process())
return workers


Expand Down Expand Up @@ -434,7 +437,7 @@ def consumer(condition, queue):

def __init__(self, lock=None):
self._condition = _Condition(
lock._lock if isinstance(lock, (Lock, RLock)) else None # type: ignore[arg-type]
lock._lock if isinstance(lock, (Lock, RLock)) else None
)

def __reduce__(self):
Expand All @@ -458,13 +461,9 @@ def acquire(self, blocking=True, timeout=None):
bool
True if acquired, False if not acquired (non-blocking or timeout).
"""
if get_backend() == "threading":
if timeout is None or timeout < 0:
timeout = -1
else:
if timeout is not None and timeout < 0:
timeout = None
return self._condition.acquire(blocking, timeout) # type: ignore[call-arg]
if timeout is None or timeout < 0:
return self._condition.acquire(blocking)
return self._condition.acquire(blocking, timeout)

def release(self):
"""
Expand Down Expand Up @@ -725,13 +724,9 @@ def acquire(self, blocking=True, timeout=None):
bool
True if acquired, False if not acquired (non-blocking or timeout).
"""
if get_backend() == "threading":
if timeout is None or timeout < 0:
timeout = -1
else:
if timeout is not None and timeout < 0:
timeout = None
return self._lock.acquire(blocking, timeout) # type: ignore[call-arg]
if timeout is None or timeout < 0:
return self._lock.acquire(blocking)
return self._lock.acquire(blocking, timeout)

def release(self):
"""
Expand All @@ -756,7 +751,7 @@ def locked(self):
True if locked, False otherwise.
"""
if hasattr(self._lock, "locked"):
return self._lock.locked() # type: ignore[attr-defined]
return self._lock.locked()

# Fallback for Python < 3.14
if self.acquire(blocking=False):
Expand Down Expand Up @@ -1042,13 +1037,9 @@ def acquire(self, blocking=True, timeout=None):
bool
True if acquired, False if not acquired (non-blocking or timeout).
"""
if get_backend() == "threading":
if timeout is None or timeout < 0:
timeout = -1
else:
if timeout is not None and timeout < 0:
timeout = None
return self._lock.acquire(blocking, timeout) # type: ignore[call-arg]
if timeout is None or timeout < 0:
return self._lock.acquire(blocking)
return self._lock.acquire(blocking, timeout)

def release(self):
"""
Expand Down Expand Up @@ -1865,7 +1856,7 @@ def _raise_unpickle_type_error():
def _validate_picklability(**kwargs):
"""Validate that all arguments are picklable for multiprocessing compatibility."""
spawning_popen = get_spawning_popen()
set_spawning_popen(_DummyPopen) # type: ignore[arg-type]
set_spawning_popen(_DummyPopen) # ty: ignore[invalid-argument-type]
try:
dump(tuple(kwargs.values()), io.BytesIO())
except (AttributeError, TypeError, pickle.PicklingError) as e:
Expand Down
38 changes: 28 additions & 10 deletions tests/test_freethreading.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,22 @@ def test_condition_with_rlock(backend):
def test_condition_acquire_release(backend):
cond = backend.Condition()
acquired = cond.acquire()
assert acquired is True
assert acquired
cond.release()


def test_condition_acquire_timeout(backend):
cond = backend.Condition()

acquired = cond.acquire(timeout=0.01)
assert acquired

cond.release()


def test_condition_acquire_negative_timeout(backend):
cond = backend.Condition()
result = cond.acquire(blocking=True, timeout=-0.5)
result = cond.acquire(blocking=True, timeout=-1)
assert result
cond.release()

Expand Down Expand Up @@ -144,7 +153,7 @@ def test_condition_wait(backend):
cond = backend.Condition()
with cond:
notified = cond.wait(timeout=0.01)
assert notified is False
assert not notified


def test_condition_wait_for(backend):
Expand Down Expand Up @@ -225,8 +234,8 @@ def test_lock_acquire_timeout(backend):
acquired = lock.acquire(timeout=0.01)
assert acquired

acquired2 = lock.acquire(blocking=False)
assert not acquired2
acquired = lock.acquire(blocking=False)
assert not acquired

lock.release()

Expand Down Expand Up @@ -348,9 +357,18 @@ def test_rlock_acquire_release(backend):
lock.release()


def test_rlock_acquire_timeout(backend):
lock = backend.RLock()

acquired = lock.acquire(timeout=0.01)
assert acquired

lock.release()


def test_rlock_acquire_negative_timeout(backend):
lock = backend.RLock()
result = lock.acquire(blocking=True, timeout=-0.5)
result = lock.acquire(blocking=True, timeout=-1)
assert result
lock.release()

Expand Down Expand Up @@ -383,8 +401,8 @@ def test_semaphore_timeout(backend):
acquired = sem.acquire(timeout=0.01)
assert acquired

acquired2 = sem.acquire(blocking=False)
assert not acquired2
acquired = sem.acquire(blocking=False)
assert not acquired

sem.release()

Expand Down Expand Up @@ -436,10 +454,10 @@ def test_worker_name_property(backend):

def test_worker_daemon_property(backend):
worker = backend.Worker(target=task, daemon=False)
assert worker.daemon is False
assert not worker.daemon

worker.daemon = True
assert worker.daemon is True
assert worker.daemon

worker.start()
worker.join()
Expand Down
Loading