2022-12-14 21:07:11 +00:00
import json
2022-09-22 09:12:59 +00:00
import os
2022-06-08 07:09:42 +00:00
import re
2022-06-26 06:09:12 +00:00
import subprocess
2022-06-08 07:09:42 +00:00
import sys
2022-11-20 13:58:41 +00:00
import tempfile
2022-02-12 09:37:46 +00:00
import time
2022-12-24 19:17:25 +00:00
from functools import lru_cache
2022-02-12 09:37:46 +00:00
from glob import glob
2022-06-08 04:59:18 +00:00
from pathlib import Path
2022-12-24 19:17:25 +00:00
from typing import Any , Dict , List , Optional , Tuple
2022-02-12 09:37:46 +00:00
import nox
nox . options . sessions = [ " test " , " clippy " , " fmt " ]
2022-12-14 21:07:11 +00:00
PYO3_DIR = Path ( __file__ ) . parent
2022-12-24 19:17:25 +00:00
PY_VERSIONS = ( " 3.7 " , " 3.8 " , " 3.9 " , " 3.10 " , " 3.11 " )
PYPY_VERSIONS = ( " 3.7 " , " 3.8 " , " 3.9 " )
2022-12-14 21:07:11 +00:00
2022-02-12 09:37:46 +00:00
@nox.session ( venv_backend = " none " )
2022-09-22 09:12:59 +00:00
def test ( session : nox . Session ) - > None :
2022-02-12 09:37:46 +00:00
test_rust ( session )
test_py ( session )
@nox.session ( name = " test-rust " , venv_backend = " none " )
def test_rust ( session : nox . Session ) :
2022-09-22 09:12:59 +00:00
_run_cargo_test ( session , package = " pyo3-build-config " )
_run_cargo_test ( session , package = " pyo3-macros-backend " )
_run_cargo_test ( session , package = " pyo3-macros " )
_run_cargo_test ( session , package = " pyo3-ffi " )
_run_cargo_test ( session )
_run_cargo_test ( session , features = " abi3 " )
2022-10-28 20:04:11 +00:00
if not " skip-full " in session . posargs :
_run_cargo_test ( session , features = " full " )
_run_cargo_test ( session , features = " abi3 full " )
2022-02-12 09:37:46 +00:00
@nox.session ( name = " test-py " , venv_backend = " none " )
2022-09-22 09:12:59 +00:00
def test_py ( session : nox . Session ) - > None :
_run ( session , " nox " , " -f " , " pytests/noxfile.py " , external = True )
2022-02-12 09:37:46 +00:00
for example in glob ( " examples/*/noxfile.py " ) :
2022-09-22 09:12:59 +00:00
_run ( session , " nox " , " -f " , example , external = True )
@nox.session ( venv_backend = " none " )
def coverage ( session : nox . Session ) - > None :
session . env . update ( _get_coverage_env ( ) )
_run ( session , " cargo " , " llvm-cov " , " clean " , " --workspace " , external = True )
test ( session )
_run (
session ,
" cargo " ,
" llvm-cov " ,
" --package=pyo3 " ,
" --package=pyo3-build-config " ,
" --package=pyo3-macros-backend " ,
" --package=pyo3-macros " ,
" --package=pyo3-ffi " ,
" report " ,
" --lcov " ,
" --output-path " ,
" coverage.lcov " ,
external = True ,
)
2022-02-12 09:37:46 +00:00
@nox.session
def fmt ( session : nox . Session ) :
fmt_rust ( session )
fmt_py ( session )
@nox.session ( name = " fmt-rust " , venv_backend = " none " )
def fmt_rust ( session : nox . Session ) :
2022-09-22 09:12:59 +00:00
_run ( session , " cargo " , " fmt " , " --all " , " --check " , external = True )
2022-02-12 09:37:46 +00:00
@nox.session ( name = " fmt-py " )
def fmt_py ( session : nox . Session ) :
2022-03-30 03:22:26 +00:00
session . install ( " black==22.3.0 " )
2022-09-22 09:12:59 +00:00
_run ( session , " black " , " . " , " --check " )
2022-02-12 09:37:46 +00:00
2022-12-24 19:17:25 +00:00
@nox.session ( name = " clippy " , venv_backend = " none " )
def clippy ( session : nox . Session ) - > bool :
if not _clippy ( session ) :
session . error ( " one or more jobs failed " )
def _clippy ( session : nox . Session , * , env : Dict [ str , str ] = None ) - > bool :
success = True
env = env or os . environ
for feature_set in _get_feature_sets ( ) :
command = " clippy "
extra = ( " -- " , " --deny=warnings " )
if _get_rust_version ( ) [ : 2 ] == ( 1 , 48 ) :
# 1.48 crashes during clippy because of lints requested
# in .cargo/config
command = " check "
extra = ( )
try :
_run (
session ,
" cargo " ,
command ,
* feature_set ,
" --all-targets " ,
" --workspace " ,
2022-12-17 11:32:56 +00:00
# linting pyo3-ffi-check requires docs to have been built or
# the macros will error; doesn't seem worth it on CI
" --exclude=pyo3-ffi-check " ,
2022-12-24 19:17:25 +00:00
* extra ,
external = True ,
env = env ,
)
except Exception :
success = False
return success
@nox.session ( name = " clippy-all " , venv_backend = " none " )
def clippy_all ( session : nox . Session ) - > None :
success = True
with tempfile . NamedTemporaryFile ( " r+ " ) as config :
env = os . environ . copy ( )
env [ " PYO3_CONFIG_FILE " ] = config . name
env [ " PYO3_CI " ] = " 1 "
def _clippy_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 ( )
session . log ( f " { implementation } { version } " )
return _clippy ( session , env = env )
for version in PY_VERSIONS :
success & = _clippy_with_config ( " CPython " , version )
for version in PYPY_VERSIONS :
success & = _clippy_with_config ( " PyPy " , version )
if not success :
session . error ( " one or more jobs failed " )
2022-02-12 09:37:46 +00:00
@nox.session ( venv_backend = " none " )
def publish ( session : nox . Session ) - > None :
2022-09-22 09:12:59 +00:00
_run_cargo_publish ( session , package = " pyo3-build-config " )
2022-02-12 09:37:46 +00:00
time . sleep ( 10 )
2022-09-22 09:12:59 +00:00
_run_cargo_publish ( session , package = " pyo3-macros-backend " )
2022-02-12 09:37:46 +00:00
time . sleep ( 10 )
2022-09-22 09:12:59 +00:00
_run_cargo_publish ( session , package = " pyo3-macros " )
2022-02-12 09:37:46 +00:00
time . sleep ( 10 )
2022-09-22 09:12:59 +00:00
_run_cargo_publish ( session , package = " pyo3-ffi " )
2022-02-12 09:37:46 +00:00
time . sleep ( 10 )
2022-09-22 09:12:59 +00:00
_run_cargo_publish ( session , package = " pyo3 " )
2022-04-11 06:57:00 +00:00
@nox.session ( venv_backend = " none " )
def contributors ( session : nox . Session ) - > None :
import requests
2022-09-29 07:30:11 +00:00
if len ( session . posargs ) < 1 :
2022-04-11 06:57:00 +00:00
raise Exception ( " base commit positional argument missing " )
base = session . posargs [ 0 ]
page = 1
2022-09-29 07:30:11 +00:00
head = " HEAD "
if len ( session . posargs ) == 2 :
head = session . posargs [ 1 ]
if len ( session . posargs ) > 2 :
raise Exception ( " too many arguments " )
2022-04-11 06:57:00 +00:00
authors = set ( )
while True :
resp = requests . get (
2022-09-29 07:30:11 +00:00
f " https://api.github.com/repos/PyO3/pyo3/compare/ { base } ... { head } " ,
2022-04-11 06:57:00 +00:00
params = { " page " : page , " per_page " : 100 } ,
)
body = resp . json ( )
if resp . status_code != 200 :
raise Exception (
f " failed to retrieve commits: { resp . status_code } { body [ ' message ' ] } "
)
for commit in body [ " commits " ] :
try :
authors . add ( commit [ " author " ] [ " login " ] )
except :
continue
if " next " in resp . links :
page + = 1
else :
break
2022-04-14 06:42:53 +00:00
authors = sorted ( list ( authors ) , key = lambda author : author . lower ( ) )
2022-04-11 06:57:00 +00:00
for author in authors :
print ( f " @ { author } " )
2022-06-08 04:59:18 +00:00
class EmscriptenInfo :
def __init__ ( self ) :
2022-12-14 21:07:11 +00:00
self . emscripten_dir = PYO3_DIR / " emscripten "
self . builddir = PYO3_DIR / " .nox/emscripten "
2022-06-08 04:59:18 +00:00
self . builddir . mkdir ( exist_ok = True , parents = True )
2022-06-08 07:09:42 +00:00
self . pyversion = sys . version . split ( ) [ 0 ]
2022-06-08 04:59:18 +00:00
self . pymajor , self . pyminor , self . pymicro = self . pyversion . split ( " . " )
self . pymicro , self . pydev = re . match (
" ([0-9]*)([^0-9].*)? " , self . pymicro
) . groups ( )
if self . pydev is None :
self . pydev = " "
self . pymajorminor = f " { self . pymajor } . { self . pyminor } "
self . pymajorminormicro = f " { self . pymajorminor } . { self . pymicro } "
2022-06-08 07:09:42 +00:00
@nox.session ( name = " build-emscripten " , venv_backend = " none " )
2022-06-08 04:59:18 +00:00
def build_emscripten ( session : nox . Session ) :
info = EmscriptenInfo ( )
2022-09-22 09:12:59 +00:00
_run (
session ,
2022-06-08 04:59:18 +00:00
" make " ,
" -C " ,
str ( info . emscripten_dir ) ,
f " BUILDROOT= { info . builddir } " ,
f " PYMAJORMINORMICRO= { info . pymajorminormicro } " ,
f " PYPRERELEASE= { info . pydev } " ,
external = True ,
)
2022-06-08 07:09:42 +00:00
@nox.session ( name = " test-emscripten " , venv_backend = " none " )
2022-06-08 04:59:18 +00:00
def test_emscripten ( session : nox . Session ) :
info = EmscriptenInfo ( )
libdir = info . builddir / f " install/Python- { info . pyversion } /lib "
pythonlibdir = libdir / f " python { info . pymajorminor } "
target = " wasm32-unknown-emscripten "
session . env [ " CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER " ] = " python " + str (
info . emscripten_dir / " runner.py "
)
session . env [ " RUSTFLAGS " ] = " " . join (
[
f " -L native= { libdir } " ,
" -C link-arg=--preload-file " ,
f " -C link-arg= { pythonlibdir } @/lib/python { info . pymajorminor } " ,
f " -C link-arg=-lpython { info . pymajorminor } " ,
" -C link-arg=-lexpat " ,
" -C link-arg=-lmpdec " ,
2022-10-29 15:24:37 +00:00
" -C link-arg=-lz " ,
" -C link-arg=-lbz2 " ,
2022-07-15 19:39:58 +00:00
" -C link-arg=-sALLOW_MEMORY_GROWTH=1 " ,
2022-06-08 04:59:18 +00:00
]
)
session . env [ " CARGO_BUILD_TARGET " ] = target
session . env [ " PYO3_CROSS_LIB_DIR " ] = pythonlibdir
2022-09-22 09:12:59 +00:00
_run ( session , " rustup " , " target " , " add " , target , " --toolchain " , " stable " )
_run (
session ,
" bash " ,
" -c " ,
f " source { info . builddir / ' emsdk/emsdk_env.sh ' } && cargo test " ,
2022-06-08 04:59:18 +00:00
)
2022-06-08 13:30:11 +00:00
@nox.session ( name = " build-guide " , venv_backend = " none " )
def build_guide ( session : nox . Session ) :
2022-09-22 09:12:59 +00:00
_run ( session , " mdbook " , " build " , " -d " , " ../target/guide " , " guide " , * session . posargs )
2022-06-26 06:09:12 +00:00
2022-11-20 13:58:41 +00:00
@nox.session ( name = " format-guide " , venv_backend = " none " )
def format_guide ( session : nox . Session ) :
fence_line = " //! ``` \n "
for path in Path ( " guide " ) . glob ( " **/*.md " ) :
session . log ( " Working on %s " , path )
content = path . read_text ( )
lines = iter ( path . read_text ( ) . splitlines ( True ) )
new_lines = [ ]
for line in lines :
new_lines . append ( line )
if not re . search ( " ```rust(,.*)?$ " , line ) :
continue
# Found a code block fence, gobble up its lines and write to temp. file
prefix = line [ : line . index ( " ``` " ) ]
with tempfile . NamedTemporaryFile ( " w " , delete = False ) as file :
tempname = file . name
file . write ( fence_line )
for line in lines :
if line == prefix + " ``` \n " :
break
file . write ( ( " //! " + line [ len ( prefix ) : ] ) . rstrip ( ) + " \n " )
file . write ( fence_line )
# Format it (needs nightly rustfmt for `format_code_in_doc_comments`)
_run (
session ,
" rustfmt " ,
" +nightly " ,
" --config " ,
" format_code_in_doc_comments=true " ,
" --config " ,
" reorder_imports=false " ,
tempname ,
)
# Re-read the formatted file, add its lines, and delete it
with open ( tempname , " r " ) as file :
for line in file :
if line == fence_line :
continue
new_lines . append ( ( prefix + line [ 4 : ] ) . rstrip ( ) + " \n " )
os . unlink ( tempname )
new_lines . append ( prefix + " ``` \n " )
path . write_text ( " " . join ( new_lines ) )
2022-06-26 06:09:12 +00:00
@nox.session ( name = " address-sanitizer " , venv_backend = " none " )
def address_sanitizer ( session : nox . Session ) :
2022-09-22 09:12:59 +00:00
_run (
session ,
2022-06-26 06:09:12 +00:00
" cargo " ,
" +nightly " ,
" test " ,
2022-10-30 08:59:36 +00:00
" --release " ,
2022-10-28 20:04:11 +00:00
" -Zbuild-std " ,
2022-06-26 06:09:12 +00:00
f " --target= { _get_rust_target ( ) } " ,
" -- " ,
" --test-threads=1 " ,
env = {
" RUSTFLAGS " : " -Zsanitizer=address " ,
" RUSTDOCFLAGS " : " -Zsanitizer=address " ,
" ASAN_OPTIONS " : " detect_leaks=0 " ,
} ,
external = True ,
)
2022-12-14 21:07:11 +00:00
@nox.session ( name = " check-changelog " )
def check_changelog ( session : nox . Session ) :
event_path = os . environ . get ( " GITHUB_EVENT_PATH " )
if event_path is None :
session . error ( " Can only check changelog on github actions " )
with open ( event_path ) as event_file :
event = json . load ( event_file )
if event [ " pull_request " ] [ " title " ] . startswith ( " release: " ) :
session . skip ( " PR title starts with release " )
for label in event [ " pull_request " ] [ " labels " ] :
if label [ " name " ] == " CI-skip-changelog " :
session . skip ( " CI-skip-changelog label applied " )
issue_number = event [ " pull_request " ] [ " number " ]
newsfragments = PYO3_DIR / " newsfragments "
fragments = tuple (
filter (
Path . exists ,
(
newsfragments / f " { issue_number } . { change_type } .md "
for change_type in ( " packaging " , " added " , " changed " , " removed " , " fixed " )
) ,
)
)
if not fragments :
session . error (
" Changelog entry not found, please add one (or more) to `newsfragments` directory. For more information see https://github.com/PyO3/pyo3/blob/main/Contributing.md#documenting-changes "
)
print ( " Found newsfragments: " )
for fragment in fragments :
print ( fragment . name )
2022-12-25 09:05:23 +00:00
@nox.session ( name = " set-minimal-package-versions " )
2023-01-12 02:14:23 +00:00
def set_minimal_package_versions ( session : nox . Session , venv_backend = " none " ) :
from collections import defaultdict
try :
import tomllib as toml
except ImportError :
import toml
2022-12-17 11:32:56 +00:00
projects = (
None ,
" examples/decorator " ,
" examples/maturin-starter " ,
" examples/setuptools-rust-starter " ,
" examples/word-count " ,
)
2023-01-12 02:14:23 +00:00
min_pkg_versions = {
" indexmap " : " 1.6.2 " ,
" hashbrown " : " 0.9.1 " ,
" plotters " : " 0.3.1 " ,
" plotters-svg " : " 0.3.1 " ,
" plotters-backend " : " 0.3.2 " ,
" bumpalo " : " 3.10.0 " ,
" once_cell " : " 1.14.0 " ,
" rayon " : " 1.5.3 " ,
" rayon-core " : " 1.9.3 " ,
# string_cache 0.8.4 depends on parking_lot 0.12
" string_cache " : " 0.8.3 " ,
# 1.15.0 depends on hermit-abi 0.2.6 which has edition 2021 and breaks 1.48.0
" num_cpus " : " 1.14.0 " ,
" parking_lot " : " 0.11.0 " ,
2023-01-28 17:24:37 +00:00
# 1.0.77 needs basic-toml which has edition 2021
" trybuild " : " 1.0.76 " ,
2023-01-12 02:14:23 +00:00
}
2022-12-17 11:32:56 +00:00
2022-12-25 09:05:23 +00:00
# run cargo update first to ensure that everything is at highest
# possible version, so that this matches what CI will resolve to.
2022-12-17 11:32:56 +00:00
for project in projects :
if project is None :
_run ( session , " cargo " , " update " , external = True )
else :
_run (
session ,
" cargo " ,
" update " ,
f " --manifest-path= { project } /Cargo.toml " ,
external = True ,
)
2022-12-25 09:05:23 +00:00
for project in projects :
2023-01-12 02:14:23 +00:00
lock_file = Path ( project or " " ) / " Cargo.lock "
def load_pkg_versions ( ) :
cargo_lock = toml . loads ( lock_file . read_text ( ) )
# Cargo allows to depends on multiple versions of the same package
pkg_versions = defaultdict ( list )
for pkg in cargo_lock [ " package " ] :
name = pkg [ " name " ]
if name not in min_pkg_versions :
continue
pkg_versions [ name ] . append ( pkg [ " version " ] )
return pkg_versions
pkg_versions = load_pkg_versions ( )
for ( pkg_name , min_version ) in min_pkg_versions . items ( ) :
versions = pkg_versions . get ( pkg_name , [ ] )
for version in versions :
if version != min_version :
pkg_id = pkg_name + " : " + version
_run_cargo_set_package_version (
session , pkg_id , min_version , project = project
)
# assume `_run_cargo_set_package_version` has changed something
# and re-read `Cargo.lock`
pkg_versions = load_pkg_versions ( )
2022-12-25 09:05:23 +00:00
# As a smoke test, cargo metadata solves all dependencies, so
# will break if any crates rely on cargo features not
# supported on MSRV
2022-12-17 11:32:56 +00:00
for project in projects :
if project is None :
_run ( session , " cargo " , " metadata " , silent = True , external = True )
else :
_run (
session ,
" cargo " ,
" metadata " ,
f " --manifest-path= { project } /Cargo.toml " ,
silent = True ,
external = True ,
)
@nox.session ( name = " ffi-check " )
def ffi_check ( session : nox . Session ) :
session . run ( " cargo " , " doc " , " -p " , " pyo3-ffi " , " --no-deps " , external = True )
_run ( session , " cargo " , " run " , " -p " , " pyo3-ffi-check " , external = True )
2022-12-25 09:05:23 +00:00
2022-12-24 19:17:25 +00:00
@lru_cache ( )
def _get_rust_info ( ) - > Tuple [ str , . . . ] :
2022-12-14 21:07:11 +00:00
output = _get_output ( " rustc " , " -vV " )
2022-06-26 06:09:12 +00:00
2022-12-24 19:17:25 +00:00
return tuple ( output . splitlines ( ) )
def _get_rust_version ( ) - > Tuple [ int , int , int , List [ str ] ] :
for line in _get_rust_info ( ) :
if line . startswith ( _RELEASE_LINE_START ) :
version = line [ len ( _RELEASE_LINE_START ) : ] . strip ( )
# e.g. 1.67.0-beta.2
( version_number , * extra ) = version . split ( " - " , maxsplit = 1 )
return ( * map ( int , version_number . split ( " . " ) ) , extra )
def _get_rust_target ( ) - > str :
for line in _get_rust_info ( ) :
2022-06-26 06:09:12 +00:00
if line . startswith ( _HOST_LINE_START ) :
return line [ len ( _HOST_LINE_START ) : ] . strip ( )
2022-12-24 19:17:25 +00:00
@lru_cache ( )
def _get_feature_sets ( ) - > Tuple [ Tuple [ str , . . . ] , . . . ] :
""" Returns feature sets to use for clippy job """
rust_version = _get_rust_version ( )
if rust_version [ : 2 ] > = ( 1 , 62 ) :
# multiple-pymethods feature not supported before 1.62
return (
( " --no-default-features " , ) ,
(
" --no-default-features " ,
" --features=abi3 " ,
) ,
( " --features=full multiple-pymethods " , ) ,
( " --features=abi3 full multiple-pymethods " , ) ,
)
else :
return (
( " --no-default-features " , ) ,
(
" --no-default-features " ,
" --features=abi3 " ,
) ,
( " --features=full " , ) ,
( " --features=abi3 full " , ) ,
)
_RELEASE_LINE_START = " release: "
2022-06-26 06:09:12 +00:00
_HOST_LINE_START = " host: "
2022-09-22 09:12:59 +00:00
def _get_coverage_env ( ) - > Dict [ str , str ] :
env = { }
2022-12-14 21:07:11 +00:00
output = _get_output ( " cargo " , " llvm-cov " , " show-env " )
2022-09-22 09:12:59 +00:00
for line in output . strip ( ) . splitlines ( ) :
( key , value ) = line . split ( " = " , maxsplit = 1 )
env [ key ] = value . strip ( ' " ' )
# Ensure that examples/ and pytests/ all build to the correct target directory to collect
# coverage artifacts.
env [ " CARGO_TARGET_DIR " ] = env [ " CARGO_LLVM_COV_TARGET_DIR " ]
return env
def _run ( session : nox . Session , * args : str , * * kwargs : Any ) - > None :
""" Wrapper for _run(session, which creates nice groups on GitHub Actions. """
if " GITHUB_ACTIONS " in os . environ :
# Insert ::group:: at the start of nox's command line output
print ( " ::group:: " , end = " " , flush = True , file = sys . stderr )
session . run ( * args , * * kwargs )
if " GITHUB_ACTIONS " in os . environ :
print ( " ::endgroup:: " , file = sys . stderr )
def _run_cargo_test (
session : nox . Session ,
* ,
package : Optional [ str ] = None ,
features : Optional [ str ] = None ,
) - > None :
2022-10-28 20:04:11 +00:00
command = [ " cargo " ]
if " careful " in session . posargs :
command . append ( " careful " )
command . append ( " test " )
if " release " in session . posargs :
command . append ( " --release " )
2022-09-22 09:12:59 +00:00
if package :
command . append ( f " --package= { package } " )
if features :
command . append ( f " --features= { features } " )
_run ( session , * command , external = True )
def _run_cargo_publish ( session : nox . Session , * , package : str ) - > None :
_run ( session , " cargo " , " publish " , f " --package= { package } " , external = True )
2022-12-14 21:07:11 +00:00
2022-12-25 09:05:23 +00:00
def _run_cargo_set_package_version (
session : nox . Session ,
2023-01-12 02:14:23 +00:00
pkg_id : str ,
2022-12-25 09:05:23 +00:00
version : str ,
* ,
project : Optional [ str ] = None ,
) - > None :
2023-01-12 02:14:23 +00:00
command = [ " cargo " , " update " , " -p " , pkg_id , " --precise " , version ]
2022-12-25 09:05:23 +00:00
if project :
command . append ( f " --manifest-path= { project } /Cargo.toml " )
_run ( session , * command , external = True )
2022-12-14 21:07:11 +00:00
def _get_output ( * args : str ) - > str :
return subprocess . run ( args , capture_output = True , text = True , check = True ) . stdout