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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.10', '3.11', '3.12', '3.13']
python: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

## [2.0.1] — Unreleased
## [2.0.1] — 2026-10-20

### Fixed
- Converting several local files at once (`Files`) failed with `TypeError: can only concatenate str (not
"NoneType") to str` on macOS and on Python 3.14+ on Linux: the parallel uploads ran in child processes,
which do not inherit `api_credentials` under the spawn and forkserver start methods. They now run in
threads, honouring `max_parallel_uploads` as before.

### Changed
- Packaging moved from `setup.py` to `pyproject.toml`. The PyPI page now shows this README and links to
the source, changelog and issues; the wheel is `py3-none-any` (it was tagged `py2.py3`, although
Python 2 has not been supported since 1.7.0). No change to the library's API or dependencies.
- Tested on Python 3.10–3.13 (was 3.7 and 3.9), with pytest instead of nose. The tests run without network
- Tested on Python 3.10–3.14 (was 3.7 and 3.9), with pytest instead of nose. The tests run without network
access; the ones that call the real service are marked `live`.
- Releases are now cut from a GitHub Release `v{version}`; the package version comes from the tag and is
uploaded with PyPI Trusted Publishing. See [RELEASING.md](RELEASING.md).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Install from source with:

### Requirements

* Python 3.10+ (tested on 3.10–3.13)
* Python 3 with `requests`; tested on Python 3.10–3.14

## Usage

Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`main` is the only long-lived branch. Changes reach it through pull requests, merge commits only, and
every pull request and push to `main` must pass the `build` check (`.github/workflows/build.yml`: the
tests on Python 3.10–3.13, then build and check the sdist and wheel). Merging publishes nothing.
tests on Python 3.10–3.14, then build and check the sdist and wheel). Merging publishes nothing.

A release is a **GitHub Release** whose tag is the version, published by `tomasr78`:

Expand Down
14 changes: 6 additions & 8 deletions convertapi/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import multiprocessing
from concurrent.futures import ThreadPoolExecutor

# Uploads wait on the network, so threads are enough, and unlike processes they share the caller's
# configuration (api_credentials, base_uri, ...) whatever multiprocessing start method Python uses.
def map_in_parallel(f, values, pool_size):
if pool_size < 2:
return map(f, values)
return list(map(f, values))

pool = multiprocessing.Pool(pool_size)
results = pool.map_async(f, values)
pool.close()
pool.join()

return results.get()
with ThreadPoolExecutor(max_workers = pool_size) as pool:
return list(pool.map(f, values))
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Software Development :: Libraries :: Python Modules",
]

Expand Down
19 changes: 19 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,22 @@ def test_http_error_without_json_body():
convertapi.convert('pdf', { 'Url': 'https://example.com' })

assert '502' in str(error.value)


@pytest.mark.parametrize('max_parallel_uploads', [1, 10])
@responses.activate
def test_convert_files_uploads_each(max_parallel_uploads):
# Each upload must carry the caller's configuration: in 2.0.0 parallel uploads ran in child processes,
# which lost api_credentials under the spawn and forkserver start methods (macOS, Python 3.14 on Linux).
convertapi.max_parallel_uploads = max_parallel_uploads
responses.post(BASE + 'upload', json={ 'FileId': 'abc', 'FileExt': 'docx' })
responses.post(BASE + 'convert/any/to/zip', json=conversion('test.zip'))

convertapi.convert('zip', { 'Files': ['examples/files/test.docx', 'examples/files/test.pdf'] })

uploads = [call.request for call in responses.calls if call.request.url == BASE + 'upload']
assert [r.headers['Authorization'] for r in uploads] == ['Bearer test-token'] * 2
assert sorted(r.headers['Content-Disposition'] for r in uploads) == [
"attachment; filename*=UTF-8''test.docx", "attachment; filename*=UTF-8''test.pdf"]
sent = form(responses.calls[-1])
assert sent['Files[0]'] == 'abc' and sent['Files[1]'] == 'abc'
Loading