diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59f46203..e44b12b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: PYTHON: - {VERSION: "3.8", TOXENV: "py38"} - {VERSION: "3.13", TOXENV: "py313"} + - {VERSION: "3.13t", TOXENV: "py313"} MACOS: - macos-13 - macos-latest @@ -24,7 +25,7 @@ jobs: - uses: actions/checkout@v4.2.2 - name: Setup python id: setup-python - uses: actions/setup-python@v5.3.0 + uses: quansight-labs/setup-python@v5.3.1 with: python-version: ${{ matrix.PYTHON.VERSION }} - uses: actions/cache@v4.2.0 @@ -53,12 +54,13 @@ jobs: PYTHON: - {VERSION: "3.8", TOXENV: "py38"} - {VERSION: "3.13", TOXENV: "py313"} + - {VERSION: "3.13t", TOXENV: "py313"} name: "Python ${{ matrix.PYTHON.VERSION }} on ${{ matrix.WINDOWS.WINDOWS }}" steps: - uses: actions/checkout@v4.2.2 - name: Setup python id: setup-python - uses: actions/setup-python@v5.3.0 + uses: quansight-labs/setup-python@v5.3.1 with: python-version: ${{ matrix.PYTHON.VERSION }} architecture: ${{ matrix.WINDOWS.ARCH }} @@ -92,6 +94,7 @@ jobs: - {VERSION: "3.11", TOXENV: "py311"} - {VERSION: "3.12", TOXENV: "py312"} - {VERSION: "3.13", TOXENV: "py313"} + - {VERSION: "3.13t", TOXENV: "py313"} - {VERSION: "pypy-3.9", TOXENV: "pypy3"} - {VERSION: "pypy-3.10", TOXENV: "pypy3"} @@ -99,12 +102,12 @@ jobs: - {VERSION: "3.13", TOXENV: "py313", RUST_VERSION: "1.64.0"} - {VERSION: "3.13", TOXENV: "py313", RUST_VERSION: "beta"} - {VERSION: "3.13", TOXENV: "py313", RUST_VERSION: "nightly"} - name: "${{ matrix.PYTHON.TOXENV }} on linux, Rust ${{ matrix.PYTHON.RUST_VERSION || 'stable' }}" + name: "${{ matrix.PYTHON.VERSION }} on linux, Rust ${{ matrix.PYTHON.RUST_VERSION || 'stable' }}" steps: - uses: actions/checkout@v4.2.2 - name: Setup python id: setup-python - uses: actions/setup-python@v5.3.0 + uses: quansight-labs/setup-python@v5.3.1 with: python-version: ${{ matrix.PYTHON.VERSION }} - uses: actions/cache@v4.2.0 diff --git a/README.rst b/README.rst index d2e76984..86e02c10 100644 --- a/README.rst +++ b/README.rst @@ -285,7 +285,7 @@ Compatibility ------------- This library should be compatible with py-bcrypt and it will run on Python -3.6+, and PyPy 3. +3.8+ (including free-threaded builds), and PyPy 3. Security -------- diff --git a/src/_bcrypt/src/lib.rs b/src/_bcrypt/src/lib.rs index 3159bf95..127f2e78 100644 --- a/src/_bcrypt/src/lib.rs +++ b/src/_bcrypt/src/lib.rs @@ -182,7 +182,7 @@ fn kdf<'p>( }) } -#[pyo3::pymodule] +#[pyo3::pymodule(gil_used = false)] mod _bcrypt { use pyo3::types::PyModuleMethods; diff --git a/tests/test_bcrypt.py b/tests/test_bcrypt.py index b0e0182a..c447c3f6 100644 --- a/tests/test_bcrypt.py +++ b/tests/test_bcrypt.py @@ -1,3 +1,6 @@ +import uuid +from concurrent.futures import ThreadPoolExecutor + import pytest import bcrypt @@ -171,7 +174,7 @@ ] -def test_gensalt_basic(monkeypatch): +def test_gensalt_basic(): salt = bcrypt.gensalt() assert salt.startswith(b"$2b$12$") @@ -219,7 +222,7 @@ def test_gensalt_bad_prefix(): bcrypt.gensalt(prefix=b"bad") -def test_gensalt_2a_prefix(monkeypatch): +def test_gensalt_2a_prefix(): salt = bcrypt.gensalt(prefix=b"2a") assert salt.startswith(b"$2a$12$") @@ -464,6 +467,7 @@ def test_kdf_no_warn_rounds(): bcrypt.kdf(b"password", b"salt", 10, 10, True) +@pytest.mark.thread_unsafe() def test_kdf_warn_rounds(): with pytest.warns(UserWarning): bcrypt.kdf(b"password", b"salt", 10, 10) @@ -494,3 +498,41 @@ def test_2a_wraparound_bug(): ) == b"$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi" ) + + +@pytest.mark.thread_unsafe() +def test_multithreading(): + def get_id(): + return uuid.uuid4().bytes + + class User: + def __init__(self, id_, pw): + self.id_ = id_ + self.salt = bcrypt.gensalt(4) + self.hash_ = bcrypt.hashpw(pw, self.salt) + self.key = bcrypt.kdf(pw, self.salt, 32, 50) + assert self.check(pw) + + def check(self, pw): + return bcrypt.checkpw(pw, self.hash_) + + # use UUIDs as both ID and passwords + num_users = 50 + ids = [get_id() for _ in range(num_users)] + pws = {id_: get_id() for id_, _ in zip(ids, range(num_users))} + + user_creator = ThreadPoolExecutor(max_workers=4) + + def create_user(id_, pw): + return id_, User(id_, pw) + + creator_futures = [ + user_creator.submit(create_user, id_, pw) for id_, pw in pws.items() + ] + + users = [future.result() for future in creator_futures] + + for id_, user in users: + assert bcrypt.hashpw(pws[id_], user.salt) == user.hash_ + assert user.check(pws[id_]) + assert bcrypt.kdf(pws[id_], user.salt, 32, 50) == user.key diff --git a/tox.ini b/tox.ini index dfe3e5bb..844feae9 100644 --- a/tox.ini +++ b/tox.ini @@ -6,10 +6,11 @@ extras = tests deps = coverage + pytest-run-parallel passenv = RUSTUP_HOME commands = - coverage run -m pytest --strict-markers {posargs} + coverage run -m pytest --parallel-threads=10 --strict-markers {posargs} coverage combine coverage report -m --fail-under 100