Skip to content

Commit

Permalink
add maximum Python version check (#3821)
Browse files Browse the repository at this point in the history
* add maximum Python version check

* restore dependency of `pyo3-macros-backend` on `pyo3-build-config`

* fix clippy-all noxfile job
  • Loading branch information
davidhewitt committed Feb 22, 2024
1 parent 9d1b11f commit c375c8a
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 37 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,18 @@ jobs:
echo PYO3_CONFIG_FILE=$PYO3_CONFIG_FILE >> $GITHUB_ENV
- run: python3 -m nox -s test

test-version-limits:
needs: [fmt]
if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
continue-on-error: true
- uses: dtolnay/rust-toolchain@stable
- run: python3 -m pip install --upgrade pip && pip install nox
- run: python3 -m nox -s test-version-limits

conclusion:
needs:
- fmt
Expand All @@ -459,6 +471,8 @@ jobs:
- docsrs
- coverage
- emscripten
- test-debug
- test-version-limits
if: always()
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ multiple-pymethods = ["inventory", "pyo3-macros/multiple-pymethods"]
extension-module = ["pyo3-ffi/extension-module"]

# Use the Python limited API. See https://www.python.org/dev/peps/pep-0384/ for more.
abi3 = ["pyo3-build-config/abi3", "pyo3-ffi/abi3", "pyo3-macros/abi3"]
abi3 = ["pyo3-build-config/abi3", "pyo3-ffi/abi3"]

# With abi3, we can manually set the minimum Python version.
abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37", "pyo3-ffi/abi3-py37"]
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3821.packaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Check maximum version of Python at build time and for versions not yet supported require opt-in to the `abi3` stable ABI by the environment variable `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`.
92 changes: 73 additions & 19 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from contextlib import contextmanager
import json
import os
import re
Expand All @@ -7,9 +8,10 @@
from functools import lru_cache
from glob import glob
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple

import nox
import nox.command

nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"]

Expand Down Expand Up @@ -100,7 +102,7 @@ def _clippy(session: nox.Session, *, env: Dict[str, str] = None) -> bool:
"--deny=warnings",
env=env,
)
except Exception:
except nox.command.CommandFailed:
success = False
return success

Expand Down Expand Up @@ -552,6 +554,33 @@ def ffi_check(session: nox.Session):
_run_cargo(session, "run", _FFI_CHECK)


@nox.session(name="test-version-limits")
def test_version_limits(session: nox.Session):
env = os.environ.copy()
with _config_file() as config_file:
env["PYO3_CONFIG_FILE"] = config_file.name

assert "3.6" not in PY_VERSIONS
config_file.set("CPython", "3.6")
_run_cargo(session, "check", env=env, expect_error=True)

assert "3.13" not in PY_VERSIONS
config_file.set("CPython", "3.13")
_run_cargo(session, "check", env=env, expect_error=True)

# 3.13 CPython should build with forward compatibility
env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1"
_run_cargo(session, "check", env=env)

assert "3.6" not in PYPY_VERSIONS
config_file.set("PyPy", "3.6")
_run_cargo(session, "check", env=env, expect_error=True)

assert "3.11" not in PYPY_VERSIONS
config_file.set("PyPy", "3.11")
_run_cargo(session, "check", env=env, expect_error=True)


def _build_docs_for_ffi_check(session: nox.Session) -> None:
# pyo3-ffi-check needs to scrape docs of pyo3-ffi
_run_cargo(session, "doc", _FFI_CHECK, "-p", "pyo3-ffi", "--no-deps")
Expand Down Expand Up @@ -640,7 +669,13 @@ def _run(session: nox.Session, *args: str, **kwargs: Any) -> None:
print("::endgroup::", file=sys.stderr)


def _run_cargo(session: nox.Session, *args: str, **kwargs: Any) -> None:
def _run_cargo(
session: nox.Session, *args: str, expect_error: bool = False, **kwargs: Any
) -> None:
if expect_error:
if "success_codes" in kwargs:
raise ValueError("expect_error overrides success_codes")
kwargs["success_codes"] = [101]
_run(session, "cargo", *args, **kwargs, external=True)


Expand Down Expand Up @@ -688,24 +723,14 @@ def _get_output(*args: str) -> str:
def _for_all_version_configs(
session: nox.Session, job: Callable[[Dict[str, str]], None]
) -> None:
with tempfile.NamedTemporaryFile("r+") as config:
env = os.environ.copy()
env["PYO3_CONFIG_FILE"] = config.name

def _job_with_config(implementation, version) -> bool:
config.seek(0)
config.truncate(0)
config.write(
f"""\
implementation={implementation}
version={version}
suppress_build_script_link_lines=true
"""
)
config.flush()
env = os.environ.copy()
with _config_file() as config_file:
env["PYO3_CONFIG_FILE"] = config_file.name

def _job_with_config(implementation, version):
session.log(f"{implementation} {version}")
return job(env)
config_file.set(implementation, version)
job(env)

for version in PY_VERSIONS:
_job_with_config("CPython", version)
Expand All @@ -714,5 +739,34 @@ def _job_with_config(implementation, version) -> bool:
_job_with_config("PyPy", version)


class _ConfigFile:
def __init__(self, config_file) -> None:
self._config_file = config_file

def set(self, implementation: str, version: str) -> None:
"""Set the contents of this config file to the given implementation and version."""
self._config_file.seek(0)
self._config_file.truncate(0)
self._config_file.write(
f"""\
implementation={implementation}
version={version}
suppress_build_script_link_lines=true
"""
)
self._config_file.flush()

@property
def name(self) -> str:
return self._config_file.name


@contextmanager
def _config_file() -> Iterator[_ConfigFile]:
"""Creates a temporary config file which can be repeatedly set to different values."""
with tempfile.NamedTemporaryFile("r+") as config:
yield _ConfigFile(config)


_BENCHES = "--manifest-path=pyo3-benches/Cargo.toml"
_FFI_CHECK = "--manifest-path=pyo3-ffi-check/Cargo.toml"
1 change: 1 addition & 0 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,7 @@ fn have_python_interpreter() -> bool {
/// Must be called from a PyO3 crate build script.
fn is_abi3() -> bool {
cargo_env_var("CARGO_FEATURE_ABI3").is_some()
|| env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1")
}

/// Gets the minimum supported Python version from PyO3 `abi3-py*` features.
Expand Down
72 changes: 65 additions & 7 deletions pyo3-ffi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,76 @@ use pyo3_build_config::{
cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_interpreter_config,
InterpreterConfig, PythonVersion,
},
PythonImplementation,
};

/// Minimum Python version PyO3 supports.
const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 };
struct SupportedVersions {
min: PythonVersion,
max: PythonVersion,
}

const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions {
min: PythonVersion { major: 3, minor: 7 },
max: PythonVersion {
major: 3,
minor: 12,
},
};

const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions {
min: PythonVersion { major: 3, minor: 7 },
max: PythonVersion {
major: 3,
minor: 10,
},
};

fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
ensure!(
interpreter_config.version >= MINIMUM_SUPPORTED_VERSION,
"the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})",
interpreter_config.version,
MINIMUM_SUPPORTED_VERSION,
);
// This is an undocumented env var which is only really intended to be used in CI / for testing
// and development.
if std::env::var("UNSAFE_PYO3_SKIP_VERSION_CHECK").as_deref() == Ok("1") {
return Ok(());
}

match interpreter_config.implementation {
PythonImplementation::CPython => {
let versions = SUPPORTED_VERSIONS_CPYTHON;
ensure!(
interpreter_config.version >= versions.min,
"the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})",
interpreter_config.version,
versions.min,
);
ensure!(
interpreter_config.version <= versions.max || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1"),
"the configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
= help: please check if an updated version of PyO3 is available. Current version: {}\n\
= help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI",
interpreter_config.version,
versions.max,
std::env::var("CARGO_PKG_VERSION").unwrap(),
);
}
PythonImplementation::PyPy => {
let versions = SUPPORTED_VERSIONS_PYPY;
ensure!(
interpreter_config.version >= versions.min,
"the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})",
interpreter_config.version,
versions.min,
);
// PyO3 does not support abi3, so we cannot offer forward compatibility
ensure!(
interpreter_config.version <= versions.max,
"the configured PyPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
= help: please check if an updated version of PyO3 is available. Current version: {}",
interpreter_config.version,
versions.max,
std::env::var("CARGO_PKG_VERSION").unwrap()
);
}
}

Ok(())
}
Expand Down
8 changes: 3 additions & 5 deletions pyo3-macros-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,15 @@ edition = "2021"
# not to depend on proc-macro itself.
# See https://github.com/PyO3/pyo3/pull/810 for more.
[dependencies]
quote = { version = "1", default-features = false }
proc-macro2 = { version = "1", default-features = false }
heck = "0.4"
proc-macro2 = { version = "1", default-features = false }
pyo3-build-config = { path = "../pyo3-build-config", version = "=0.20.2", features = ["resolve-config"] }
quote = { version = "1", default-features = false }

[dependencies.syn]
version = "2"
default-features = false
features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"]

[features]
abi3 = []

[lints]
workspace = true
6 changes: 3 additions & 3 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::params::impl_arg_params;
use crate::pyfunction::{FunctionSignature, PyFunctionArgPyO3Attributes};
use crate::pyfunction::{PyFunctionOptions, SignatureAttribute};
use crate::quotes;
use crate::utils::{self, PythonDoc};
use crate::utils::{self, is_abi3, PythonDoc};
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use quote::{quote, quote_spanned};
Expand Down Expand Up @@ -213,8 +213,8 @@ impl CallingConvention {
} else if signature.python_signature.kwargs.is_some() {
// for functions that accept **kwargs, always prefer varargs
Self::Varargs
} else if cfg!(not(feature = "abi3")) {
// Not available in the Stable ABI as of Python 3.10
} else if !is_abi3() {
// FIXME: available in the stable ABI since 3.10
Self::Fastcall
} else {
Self::Varargs
Expand Down
4 changes: 4 additions & 0 deletions pyo3-macros-backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,7 @@ pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String {
RenamingRule::Uppercase => name.to_uppercase(),
}
}

pub(crate) fn is_abi3() -> bool {
pyo3_build_config::get().abi3
}
2 changes: 0 additions & 2 deletions pyo3-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ proc-macro = true
[features]
multiple-pymethods = []

abi3 = ["pyo3-macros-backend/abi3"]

[dependencies]
proc-macro2 = { version = "1", default-features = false }
quote = "1"
Expand Down

0 comments on commit c375c8a

Please sign in to comment.