Skip to content

Commit ee64f1d

Browse files
BryanFaubledanlu1
andauthored
[GEN-2381] Pandas handling of nullable cells, [SYNPY-1690] Adding support for Python 3.14 and dropping support for python 3.9 (#1272)
[GEN-2381] Pandas handling of nullable cells [SYNPY-1690] Adding support for Python 3.14 and dropping support for python 3.9 --------- Co-authored-by: danlu1 <dan.lu@sagebase.org>
1 parent 49be7cc commit ee64f1d

79 files changed

Lines changed: 5185 additions & 2423 deletions

File tree

Some content is hidden

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

.github/workflows/build.yml

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555

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

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

@@ -109,7 +109,7 @@ jobs:
109109
pytest -sv --cov-append --cov=. --cov-report xml tests/unit
110110
- name: Check for Secret availability
111111
id: secret-check
112-
if: ${{ contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python) }}
112+
if: ${{ contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python) }}
113113
# perform secret check & put boolean result as an output
114114
shell: bash
115115
run: |
@@ -126,7 +126,7 @@ jobs:
126126
fi
127127
128128
- name: Download Failed Tests from Previous Attempt
129-
if: ${{ github.run_attempt > 1 && (contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
129+
if: ${{ github.run_attempt > 1 && (contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
130130
uses: actions/download-artifact@v4
131131
with:
132132
name: failed-tests-${{ matrix.os }}-${{ matrix.python }}
@@ -142,25 +142,25 @@ jobs:
142142
shell: bash
143143

144144
# keep versions consistent with the first and last from the strategy matrix
145-
if: ${{ (contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
145+
if: ${{ (contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
146146
run: |
147147
# Set SYNAPSE_PROFILE based on OS and Python version
148148
if [ "${{ startsWith(matrix.os, 'ubuntu') }}" == "true" ]; then
149-
if [ "${{ matrix.python }}" == "3.9" ]; then
149+
if [ "${{ matrix.python }}" == "3.10" ]; then
150150
export SYNAPSE_PROFILE="TestUbuntuMinimumPython"
151-
elif [ "${{ matrix.python }}" == "3.13" ]; then
151+
elif [ "${{ matrix.python }}" == "3.14" ]; then
152152
export SYNAPSE_PROFILE="TestUbuntuMaximumPython"
153153
fi
154154
elif [ "${{ startsWith(matrix.os, 'windows') }}" == "true" ]; then
155-
if [ "${{ matrix.python }}" == "3.9" ]; then
155+
if [ "${{ matrix.python }}" == "3.10" ]; then
156156
export SYNAPSE_PROFILE="TestWindowsMinimumPython"
157-
elif [ "${{ matrix.python }}" == "3.13" ]; then
157+
elif [ "${{ matrix.python }}" == "3.14" ]; then
158158
export SYNAPSE_PROFILE="TestWindowsMaximumPython"
159159
fi
160160
elif [ "${{ startsWith(matrix.os, 'macos') }}" == "true" ]; then
161-
if [ "${{ matrix.python }}" == "3.9" ]; then
161+
if [ "${{ matrix.python }}" == "3.10" ]; then
162162
export SYNAPSE_PROFILE="TestMacosMinimumPython"
163-
elif [ "${{ matrix.python }}" == "3.13" ]; then
163+
elif [ "${{ matrix.python }}" == "3.14" ]; then
164164
export SYNAPSE_PROFILE="TestMacosMaximumPython"
165165
fi
166166
fi
@@ -202,16 +202,6 @@ jobs:
202202
# Setup ignore patterns based on Python version
203203
IGNORE_FLAGS="--ignore=tests/integration/synapseclient/test_command_line_client.py"
204204
205-
if [ "${{ matrix.python }}" == "3.9" ]; then
206-
# For min Python version, ignore async tests
207-
IGNORE_FLAGS="$IGNORE_FLAGS --ignore=tests/integration/synapseclient/models/async/"
208-
echo "Running integration tests for Min Python version (3.9) - ignoring async tests"
209-
elif [ "${{ matrix.python }}" == "3.13" ]; then
210-
# For max Python version, ignore synchronous tests
211-
IGNORE_FLAGS="$IGNORE_FLAGS --ignore=tests/integration/synapseclient/models/synchronous/"
212-
echo "Running integration tests for Max Python version (3.13) - ignoring synchronous tests"
213-
fi
214-
215205
# Check if we should run only failed tests from previous attempt
216206
if [[ -f failed_tests.txt && -s failed_tests.txt ]]; then
217207
echo "::notice::Retry attempt ${{ github.run_attempt }} detected - running only previously failed tests"
@@ -362,7 +352,7 @@ jobs:
362352

363353
- uses: actions/setup-python@v5
364354
with:
365-
python-version: 3.9
355+
python-version: 3.10
366356

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

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

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

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

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ virtualenvs
66
examples_temp
77
.DS_Store
88
deploy.sh
9+
.venv/
910

1011
junk/
1112
nose.cfg

CONTRIBUTING.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ copied to forks).
9393
#### Installing the Python Client in a virtual environment with pipenv
9494
Perform the following one-time steps to set up your local environment.
9595
96-
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.
96+
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.
9797
9898
2. Install `pipenv` by running `pip install pipenv`.
9999
- If you already have `pipenv` installed, ensure that the version is >=2023.9.8 to avoid compatibility issues.
@@ -223,6 +223,64 @@ When integration tests are ran in the Github CI/CD pipeline it will upload the t
223223
#### Integration testing for external collaborators
224224
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.
225225
226+
### Managing Python version changes
227+
228+
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.
229+
230+
#### Adding a new Python version
231+
232+
When adding support for a new Python version (e.g., adding Python 3.15), update the following:
233+
234+
**Code configuration files:**
235+
1. **`setup.cfg`**:
236+
- Add the new version to the `classifiers` list under `[metadata]` (e.g., `Programming Language :: Python :: 3.15`)
237+
- Update the `python_requires` constraint under `[options]` to include the new version (e.g., `>=3.10, <3.16`)
238+
239+
2. **`pyproject.toml`**:
240+
- Update the `target-version` list in the `[tool.black]` section to include the new version if needed
241+
242+
3. **`Dockerfile`**:
243+
- Update the base image to use the new Python version (e.g., `FROM python:3.15-slim`)
244+
245+
**CI/CD configuration files:**
246+
1. **`.github/workflows/build.yml`**:
247+
- Add the new version to the `python` matrix under the `test` job strategy
248+
- Ensure the new version is included in integration test runs (typically the latest version should be tested)
249+
- Update any Python version comments or documentation within the workflow
250+
251+
**Testing:**
252+
- Run the full test suite (both unit and integration tests) on the new Python version locally before submitting a PR
253+
- Verify that all CI/CD pipelines pass with the new version included
254+
255+
#### Dropping an old Python version
256+
257+
When dropping support for an old Python version (e.g., removing Python 3.10), update the following:
258+
259+
**Code configuration files:**
260+
1. **`setup.cfg`**:
261+
- Remove the old version from the `classifiers` list under `[metadata]`
262+
- Update the `python_requires` constraint under `[options]` to reflect the new minimum version (e.g., `>=3.11, <3.15`)
263+
264+
2. **`pyproject.toml`**:
265+
- Update the `target-version` list in the `[tool.black]` section to remove the old version
266+
267+
3. **`Dockerfile`**:
268+
- Ensure the base image uses a supported Python version
269+
270+
**CI/CD configuration files:**
271+
1. **`.github/workflows/build.yml`**:
272+
- Remove the old version from the `python` matrix under the `test` job strategy
273+
- Update the cache key version (e.g., increment `v28` to `v29`) to invalidate old caches
274+
275+
**Documentation:**
276+
- Update the README.md and any getting started documentation to reflect the new supported Python version range
277+
- Update CONTRIBUTING.md (this file) if it mentions specific Python versions in examples
278+
279+
**Important considerations:**
280+
- Python version changes should be coordinated with a release and clearly communicated in release notes
281+
- Breaking compatibility with a Python version is a significant change and should typically coincide with a major or minor version bump
282+
- Always test thoroughly on the minimum and maximum supported Python versions before release
283+
226284
### Asynchronous methods
227285
[Asyncio](https://docs.python.org/3/library/asyncio.html) is the future of the Synapse
228286
Python Client. As such, the expectation is that all future methods that rely on async

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9-slim
1+
FROM python:3.14-slim
22
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
33

44
RUN apt-get update \

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ or by sending an email to [python-announce+subscribe@sagebase.org](mailto:python
3636
Installation
3737
------------
3838

39-
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.
39+
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.
4040

41-
**Starting from Synapse Python client version 3.0, Synapse Python client requires Python >= 3.9**
41+
**Starting from Synapse Python client version 3.0, Synapse Python client requires Python >= 3.10**
4242

4343
### Install using pip
4444

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Asyncio and Python 3.14+ Changes
2+
3+
## Overview
4+
5+
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.
6+
7+
**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.
8+
9+
## Background: How Jupyter Notebooks Run Code
10+
11+
Jupyter notebooks run all code within an active asyncio event loop. This means:
12+
13+
- **Async functions** must be called with `await my_async_function()`
14+
- **Synchronous functions** can be called normally with `my_function()`
15+
- **You cannot use** `asyncio.run()` because asyncio intentionally prevents nested event loops (to avoid threading and deadlock issues)
16+
17+
## What Changed in Python 3.14
18+
19+
### Previous Behavior (Python 3.13 and earlier)
20+
21+
The Synapse Python Client used a library called `nest_asyncio` to work around asyncio's nested event loop restriction. This allowed us to:
22+
23+
1. Detect whether code was running in a notebook (with an active event loop) or a regular script
24+
2. Automatically call async functions from synchronous wrapper functions
25+
3. Provide a seamless synchronous API that worked in both environments
26+
27+
**This meant you could write:**
28+
```python
29+
from synapseclient import Synapse
30+
from synapseclient.models import Project
31+
32+
syn = Synapse()
33+
syn.login()
34+
35+
# This worked in notebooks AND regular scripts
36+
my_project = Project(name="My Project").get()
37+
```
38+
39+
### New Behavior (Python 3.14+)
40+
41+
Python 3.14 changed the asyncio implementation in ways that break `nest_asyncio`. The library can no longer safely monkey-patch asyncio.
42+
43+
**Impact:** The automatic async-to-sync conversion no longer works in notebooks on Python 3.14+.
44+
45+
> **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.
46+
47+
## How This Affects Your Code
48+
49+
### In Regular Python Scripts
50+
51+
**No changes needed.** Synchronous methods continue to work as before:
52+
53+
```python
54+
from synapseclient import Synapse
55+
from synapseclient.models import Project
56+
57+
syn = Synapse()
58+
syn.login()
59+
60+
# This still works in regular scripts
61+
my_project = Project(name="My Project").get()
62+
```
63+
64+
### In Jupyter Notebooks (Python 3.14+)
65+
66+
**You must use async methods directly** with `await`:
67+
68+
```python
69+
from synapseclient import Synapse
70+
from synapseclient.models import Project
71+
72+
syn = Synapse()
73+
syn.login()
74+
75+
# Use the async version with await
76+
my_project = await Project(name="My Project").get_async()
77+
```
78+
79+
### In Other Async Contexts
80+
81+
If you're calling Synapse Python Client methods from within an async function, use the async methods:
82+
83+
```python
84+
import asyncio
85+
from synapseclient import Synapse
86+
from synapseclient.models import Project
87+
88+
syn = Synapse()
89+
syn.login()
90+
91+
async def main():
92+
# Use async methods with await
93+
my_project = await Project(name="My Project").get_async()
94+
95+
asyncio.run(main())
96+
```
97+
98+
## Error Messages
99+
100+
If you try to use synchronous methods in an async context on Python 3.14+, you'll see an error like:
101+
102+
```
103+
RuntimeError: Python 3.14+ detected an active event loop, which prevents automatic async-to-sync conversion.
104+
This is a limitation of asyncio in Python 3.14+.
105+
106+
To resolve this, use the async method directly:
107+
• Instead of: result = obj.method_name()
108+
• Use: result = await obj.method_name_async()
109+
110+
For Jupyter/IPython notebooks: You can use 'await' directly in cells.
111+
For other async contexts: Ensure you're in an async function and use 'await'.
112+
```
113+
114+
## Quick Reference
115+
116+
| Environment | Python 3.13 and earlier | Python 3.14+ |
117+
|-------------|------------------------|--------------|
118+
| Regular Python script | `obj.method()` | `obj.method()`|
119+
| Jupyter notebook | `obj.method()` | `await obj.method_async()` |
120+
| Inside async function | `await obj.method_async()` | `await obj.method_async()` |
121+
122+
## Finding Async Methods
123+
124+
For most synchronous methods, there is a corresponding async version with `_async` suffix:
125+
126+
- `get()``get_async()`
127+
- `store()``store_async()`
128+
- `delete()``delete_async()`
129+
130+
Check the API documentation for the complete list of async methods available.

docs/tutorials/installation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The [synapseclient](https://pypi.python.org/pypi/synapseclient/) package is avai
2121
- conda: Please follow instructions [here](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) to manage environments:
2222

2323
```bash
24-
conda create -n synapseclient python=3.9
24+
conda create -n synapseclient python=3.14
2525
conda activate synapseclient
2626

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

4545
```bash
46-
pyenv install -v 3.9.13
47-
pyenv global 3.9.13
46+
pyenv install -v 3.14.0
47+
pyenv global 3.14.0
4848
python -m venv env
4949
source env/bin/activate
5050

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ nav:
144144
- Manifest TSV: explanations/manifest_tsv.md
145145
- Benchmarking: explanations/benchmarking.md
146146
- Structuring Your Project: explanations/structuring_your_project.md
147+
- Asyncio Changes in Python 3.14: explanations/asyncio_in_python_3_14.md
147148
- News:
148149
- news.md
149150
- Contact Us: https://sagebionetworks.jira.com/servicedesk/customer/portal/9/group/16/create/206

pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ prefer-stubs=no
9393

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

9898
# Discover python modules and packages in the file system subtree.
9999
recursive=no

0 commit comments

Comments
 (0)