Skip to content
Open
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
35 changes: 12 additions & 23 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:

# if changing the below change the run-integration-tests versions and the check-deploy versions
# Make sure that we are running the integration tests on the first and last versions of the matrix
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
pytest -sv --cov-append --cov=. --cov-report xml tests/unit
- name: Check for Secret availability
id: secret-check
if: ${{ contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python) }}
if: ${{ contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python) }}
# perform secret check & put boolean result as an output
shell: bash
run: |
Expand All @@ -126,7 +126,7 @@ jobs:
fi

- name: Download Failed Tests from Previous Attempt
if: ${{ github.run_attempt > 1 && (contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
if: ${{ github.run_attempt > 1 && (contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
uses: actions/download-artifact@v4
with:
name: failed-tests-${{ matrix.os }}-${{ matrix.python }}
Expand All @@ -142,25 +142,25 @@ jobs:
shell: bash

# keep versions consistent with the first and last from the strategy matrix
if: ${{ (contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
if: ${{ (contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
run: |
# Set SYNAPSE_PROFILE based on OS and Python version
if [ "${{ startsWith(matrix.os, 'ubuntu') }}" == "true" ]; then
if [ "${{ matrix.python }}" == "3.9" ]; then
if [ "${{ matrix.python }}" == "3.10" ]; then
export SYNAPSE_PROFILE="TestUbuntuMinimumPython"
elif [ "${{ matrix.python }}" == "3.13" ]; then
elif [ "${{ matrix.python }}" == "3.14" ]; then
export SYNAPSE_PROFILE="TestUbuntuMaximumPython"
fi
elif [ "${{ startsWith(matrix.os, 'windows') }}" == "true" ]; then
if [ "${{ matrix.python }}" == "3.9" ]; then
if [ "${{ matrix.python }}" == "3.10" ]; then
export SYNAPSE_PROFILE="TestWindowsMinimumPython"
elif [ "${{ matrix.python }}" == "3.13" ]; then
elif [ "${{ matrix.python }}" == "3.14" ]; then
export SYNAPSE_PROFILE="TestWindowsMaximumPython"
fi
elif [ "${{ startsWith(matrix.os, 'macos') }}" == "true" ]; then
if [ "${{ matrix.python }}" == "3.9" ]; then
if [ "${{ matrix.python }}" == "3.10" ]; then
export SYNAPSE_PROFILE="TestMacosMinimumPython"
elif [ "${{ matrix.python }}" == "3.13" ]; then
elif [ "${{ matrix.python }}" == "3.14" ]; then
export SYNAPSE_PROFILE="TestMacosMaximumPython"
fi
fi
Expand Down Expand Up @@ -202,16 +202,6 @@ jobs:
# Setup ignore patterns based on Python version
IGNORE_FLAGS="--ignore=tests/integration/synapseclient/test_command_line_client.py"

if [ "${{ matrix.python }}" == "3.9" ]; then
# For min Python version, ignore async tests
IGNORE_FLAGS="$IGNORE_FLAGS --ignore=tests/integration/synapseclient/models/async/"
echo "Running integration tests for Min Python version (3.9) - ignoring async tests"
elif [ "${{ matrix.python }}" == "3.13" ]; then
# For max Python version, ignore synchronous tests
IGNORE_FLAGS="$IGNORE_FLAGS --ignore=tests/integration/synapseclient/models/synchronous/"
echo "Running integration tests for Max Python version (3.13) - ignoring synchronous tests"
fi

# Check if we should run only failed tests from previous attempt
if [[ -f failed_tests.txt && -s failed_tests.txt ]]; then
echo "::notice::Retry attempt ${{ github.run_attempt }} detected - running only previously failed tests"
Expand Down Expand Up @@ -362,7 +352,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: 3.9
python-version: 3.10

- name: set-release-env
shell: bash
Expand Down Expand Up @@ -459,7 +449,6 @@ jobs:
# asset_path: dist/${{ steps.build-package.outputs.bdist-package-name }}
# asset_content_type: application/zip


# re-download the built package to the appropriate pypi server.
# we upload prereleases to test.pypi.org and releases to pypi.org.
deploy:
Expand Down Expand Up @@ -500,7 +489,7 @@ jobs:
os: [ubuntu-24.04, macos-13, windows-2022]

# python versions should be consistent with the strategy matrix and the runs-integration-tests versions
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]

runs-on: ${{ matrix.os }}

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ virtualenvs
examples_temp
.DS_Store
deploy.sh
.venv/

junk/
nose.cfg
Expand Down
60 changes: 59 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ copied to forks).
#### Installing the Python Client in a virtual environment with pipenv
Perform the following one-time steps to set up your local environment.

1. This package uses Python, if you have not already, please install [pyenv](https://github.com/pyenv/pyenv#installation) to manage your Python versions. Versions supported by this package are all versions >=3.9 and <=3.13. If you do not install `pyenv` make sure that Python and `pip` are installed correctly and have been added to your PATH by running `python3 --version` and `pip3 --version`. If your installation was successful, your terminal will return the versions of Python and `pip` that you installed. **Note**: If you have `pyenv` it will install a specific version of Python for you.
1. This package uses Python, if you have not already, please install [pyenv](https://github.com/pyenv/pyenv#installation) to manage your Python versions. Versions supported by this package are all versions >=3.10 and <=3.14. If you do not install `pyenv` make sure that Python and `pip` are installed correctly and have been added to your PATH by running `python3 --version` and `pip3 --version`. If your installation was successful, your terminal will return the versions of Python and `pip` that you installed. **Note**: If you have `pyenv` it will install a specific version of Python for you.

2. Install `pipenv` by running `pip install pipenv`.
- If you already have `pipenv` installed, ensure that the version is >=2023.9.8 to avoid compatibility issues.
Expand Down Expand Up @@ -223,6 +223,64 @@ When integration tests are ran in the Github CI/CD pipeline it will upload the t
#### Integration testing for external collaborators
As an external collaborator you will not have access to a development account and environment to run the integration tests against. Either request that a Sage Bionetworks staff member run your integration tests via a pull request, or, contact us via the [Service Desk](https://sagebionetworks.jira.com/servicedesk/customer/portal/9) to requisition a development account for integration testing only.

### Managing Python version changes

When adding support for a new Python version or dropping support for an old version, several files across the codebase and CI/CD pipelines must be updated to ensure consistency and proper testing coverage.

#### Adding a new Python version

When adding support for a new Python version (e.g., adding Python 3.15), update the following:

**Code configuration files:**
1. **`setup.cfg`**:
- Add the new version to the `classifiers` list under `[metadata]` (e.g., `Programming Language :: Python :: 3.15`)
- Update the `python_requires` constraint under `[options]` to include the new version (e.g., `>=3.10, <3.16`)

2. **`pyproject.toml`**:
- Update the `target-version` list in the `[tool.black]` section to include the new version if needed

3. **`Dockerfile`**:
- Update the base image to use the new Python version (e.g., `FROM python:3.15-slim`)

**CI/CD configuration files:**
1. **`.github/workflows/build.yml`**:
- Add the new version to the `python` matrix under the `test` job strategy
- Ensure the new version is included in integration test runs (typically the latest version should be tested)
- Update any Python version comments or documentation within the workflow

**Testing:**
- Run the full test suite (both unit and integration tests) on the new Python version locally before submitting a PR
- Verify that all CI/CD pipelines pass with the new version included

#### Dropping an old Python version

When dropping support for an old Python version (e.g., removing Python 3.10), update the following:

**Code configuration files:**
1. **`setup.cfg`**:
- Remove the old version from the `classifiers` list under `[metadata]`
- Update the `python_requires` constraint under `[options]` to reflect the new minimum version (e.g., `>=3.11, <3.15`)

2. **`pyproject.toml`**:
- Update the `target-version` list in the `[tool.black]` section to remove the old version

3. **`Dockerfile`**:
- Ensure the base image uses a supported Python version

**CI/CD configuration files:**
1. **`.github/workflows/build.yml`**:
- Remove the old version from the `python` matrix under the `test` job strategy
- Update the cache key version (e.g., increment `v28` to `v29`) to invalidate old caches

**Documentation:**
- Update the README.md and any getting started documentation to reflect the new supported Python version range
- Update CONTRIBUTING.md (this file) if it mentions specific Python versions in examples

**Important considerations:**
- Python version changes should be coordinated with a release and clearly communicated in release notes
- Breaking compatibility with a Python version is a significant change and should typically coincide with a major or minor version bump
- Always test thoroughly on the minimum and maximum supported Python versions before release

### Asynchronous methods
[Asyncio](https://docs.python.org/3/library/asyncio.html) is the future of the Synapse
Python Client. As such, the expectation is that all future methods that rely on async
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.9-slim
FROM python:3.14-slim
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections

RUN apt-get update \
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ or by sending an email to [python-announce+subscribe@sagebase.org](mailto:python
Installation
------------

The Python Synapse client has been tested on versions 3.9, 3.10, 3.11, 3.12 and 3.13 on Mac OS X, Ubuntu Linux and Windows.
The Python Synapse client has been tested on versions 3.10, 3.11, 3.12, 3.13 and 3.14 on Mac OS X, Ubuntu Linux and Windows.

**Starting from Synapse Python client version 3.0, Synapse Python client requires Python >= 3.9**
**Starting from Synapse Python client version 3.0, Synapse Python client requires Python >= 3.10**

### Install using pip

Expand Down
130 changes: 130 additions & 0 deletions docs/explanations/asyncio_in_python_3_14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Asyncio and Python 3.14+ Changes

## Overview

Starting with Python 3.14, changes to the asyncio implementation affect how the Synapse Python Client works in Jupyter notebooks and other async contexts. This document explains what changed, why, and how it impacts your code.

**TL;DR:** Python 3.14+ breaks the `nest_asyncio` workaround. In Jupyter notebooks, you must now use async methods with `await` (e.g., `await obj.get_async()` instead of `obj.get()`). Regular scripts are unaffected.

## Background: How Jupyter Notebooks Run Code

Jupyter notebooks run all code within an active asyncio event loop. This means:

- **Async functions** must be called with `await my_async_function()`
- **Synchronous functions** can be called normally with `my_function()`
- **You cannot use** `asyncio.run()` because asyncio intentionally prevents nested event loops (to avoid threading and deadlock issues)

## What Changed in Python 3.14

### Previous Behavior (Python 3.13 and earlier)

The Synapse Python Client used a library called `nest_asyncio` to work around asyncio's nested event loop restriction. This allowed us to:

1. Detect whether code was running in a notebook (with an active event loop) or a regular script
2. Automatically call async functions from synchronous wrapper functions
3. Provide a seamless synchronous API that worked in both environments

**This meant you could write:**
```python
from synapseclient import Synapse
from synapseclient.models import Project

syn = Synapse()
syn.login()

# This worked in notebooks AND regular scripts
my_project = Project(name="My Project").get()
```

### New Behavior (Python 3.14+)

Python 3.14 changed the asyncio implementation in ways that break `nest_asyncio`. The library can no longer safely monkey-patch asyncio.

**Impact:** The automatic async-to-sync conversion no longer works in notebooks on Python 3.14+.

> **Why the change?** Python 3.14 improved asyncio's safety to prevent deadlocks and race conditions. The `nest_asyncio` workaround bypassed these safety mechanisms and now causes failures in HTTP connection pooling. Rather than risk silent failures, the library raises clear errors when async methods should be used.

## How This Affects Your Code

### In Regular Python Scripts

**No changes needed.** Synchronous methods continue to work as before:

```python
from synapseclient import Synapse
from synapseclient.models import Project

syn = Synapse()
syn.login()

# This still works in regular scripts
my_project = Project(name="My Project").get()
```

### In Jupyter Notebooks (Python 3.14+)

**You must use async methods directly** with `await`:

```python
from synapseclient import Synapse
from synapseclient.models import Project

syn = Synapse()
syn.login()

# Use the async version with await
my_project = await Project(name="My Project").get_async()
```

### In Other Async Contexts

If you're calling Synapse Python Client methods from within an async function, use the async methods:

```python
import asyncio
from synapseclient import Synapse
from synapseclient.models import Project

syn = Synapse()
syn.login()

async def main():
# Use async methods with await
my_project = await Project(name="My Project").get_async()

asyncio.run(main())
```

## Error Messages

If you try to use synchronous methods in an async context on Python 3.14+, you'll see an error like:

```
RuntimeError: Python 3.14+ detected an active event loop, which prevents automatic async-to-sync conversion.
This is a limitation of asyncio in Python 3.14+.

To resolve this, use the async method directly:
• Instead of: result = obj.method_name()
• Use: result = await obj.method_name_async()

For Jupyter/IPython notebooks: You can use 'await' directly in cells.
For other async contexts: Ensure you're in an async function and use 'await'.
```

## Quick Reference

| Environment | Python 3.13 and earlier | Python 3.14+ |
|-------------|------------------------|--------------|
| Regular Python script | `obj.method()` | `obj.method()` ✓ |
| Jupyter notebook | `obj.method()` | `await obj.method_async()` |
| Inside async function | `await obj.method_async()` | `await obj.method_async()` |

## Finding Async Methods

For most synchronous methods, there is a corresponding async version with `_async` suffix:

- `get()` → `get_async()`
- `store()` → `store_async()`
- `delete()` → `delete_async()`

Check the API documentation for the complete list of async methods available.
6 changes: 3 additions & 3 deletions docs/tutorials/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The [synapseclient](https://pypi.python.org/pypi/synapseclient/) package is avai
- conda: Please follow instructions [here](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) to manage environments:

```bash
conda create -n synapseclient python=3.9
conda create -n synapseclient python=3.14
conda activate synapseclient

# Here are a few ways to install the client. Choose the one that fits your use-case
Expand All @@ -43,8 +43,8 @@ to the `/usr/local/lib` directory. [See here](https://github.com/conda/conda/iss
- pyenv: Use [virtualenv](https://virtualenv.pypa.io/en/latest/) to manage your python environment:

```bash
pyenv install -v 3.9.13
pyenv global 3.9.13
pyenv install -v 3.14.0
pyenv global 3.14.0
python -m venv env
source env/bin/activate

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ nav:
- Manifest TSV: explanations/manifest_tsv.md
- Benchmarking: explanations/benchmarking.md
- Structuring Your Project: explanations/structuring_your_project.md
- Asyncio Changes in Python 3.14: explanations/asyncio_in_python_3_14.md
- News:
- news.md
- Contact Us: https://sagebionetworks.jira.com/servicedesk/customer/portal/9/group/16/create/206
Expand Down
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ prefer-stubs=no

# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.9
py-version=3.10

# Discover python modules and packages in the file system subtree.
recursive=no
Expand Down
Loading
Loading