From 16ad15e04fd4dd283ef3ffb7e45d68c12637de96 Mon Sep 17 00:00:00 2001
From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com>
Date: Fri, 18 Mar 2022 23:13:23 +0100
Subject: [PATCH 1/9] Expand on xtask (#2176)
* Fix Windows OSError
* Ignore .pyd files
* Put things in modules
* Rename functions to `run`
* Expand on cargo xtask
* Try to work around missing installs
* Run all things by default, but not llvm-cov
* Test msrv
* Fix more OSErrors
* Various refinements and docs
* Various refinements
* Various refinements
---
.cargo/config | 8 +-
.github/pull_request_template.md | 9 +-
.github/workflows/guide.yml | 4 +-
.gitignore | 1 +
Contributing.md | 6 +-
pytests/tests/test_datetime.py | 5 +-
xtask/Cargo.toml | 4 +
xtask/README.md | 23 ++++
xtask/src/cli.rs | 201 ++++++++++++++++++++++++++++++
xtask/src/clippy.rs | 25 ++++
xtask/src/doc.rs | 47 +++++++
xtask/src/fmt.rs | 23 ++++
xtask/src/llvm_cov.rs | 100 +++++++++++++++
xtask/src/main.rs | 206 +++----------------------------
xtask/src/pytests.rs | 27 ++++
xtask/src/test.rs | 73 +++++++++++
xtask/src/utils.rs | 65 ++++++++++
17 files changed, 620 insertions(+), 207 deletions(-)
create mode 100644 xtask/README.md
create mode 100644 xtask/src/cli.rs
create mode 100644 xtask/src/clippy.rs
create mode 100644 xtask/src/doc.rs
create mode 100644 xtask/src/fmt.rs
create mode 100644 xtask/src/llvm_cov.rs
create mode 100644 xtask/src/pytests.rs
create mode 100644 xtask/src/test.rs
create mode 100644 xtask/src/utils.rs
diff --git a/.cargo/config b/.cargo/config
index 83ef5882..68542cc9 100644
--- a/.cargo/config
+++ b/.cargo/config
@@ -1,11 +1,5 @@
[alias]
xtask = "run --package xtask --"
-pyo3_doc = "doc --lib --no-default-features --features=full --no-deps --workspace --open --exclude pyo3-macros --exclude pyo3-macros-backend"
-pyo3_doc_scrape = "doc --lib --no-default-features --features=full --no-deps --workspace --open --exclude pyo3-macros --exclude pyo3-macros-backend -Z unstable-options -Z rustdoc-scrape-examples=examples"
-pyo3_doc_internal = "doc --lib --no-default-features --features=full --no-deps --workspace --open --document-private-items -Z unstable-options -Z rustdoc-scrape-examples=examples"
-
-[build]
-rustdocflags = ["--cfg", "docsrs"]
[target.'cfg(feature = "cargo-clippy")']
rustflags = [
@@ -21,4 +15,4 @@ rustflags = [
"-Dclippy::todo",
"-Dclippy::unnecessary_wraps",
"-Dclippy::useless_transmute",
-]
+]
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 8a3782ca..077f8ec1 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -5,9 +5,6 @@ Please consider adding the following to your pull request:
- docs to all new functions and / or detail in the guide
- tests for all new or changed functions
-Be aware the CI pipeline will check your pull request for the following. This is done using `nox` (you can install with `pip install nox`):
- - Rust tests (`cargo test` or `nox -s test-rust`)
- - Examples (`nox -s test-py`)
- - Rust lints (`nox -s clippy`)
- - Rust formatting (`nox -s fmt-rust`)
- - Python formatting (`nox -s fmt-py`)
+PyO3's CI pipeline will check your pull request. To run its tests
+locally, you can run ```cargo xtask ci```. See its documentation
+ [here](https://github.com/PyO3/pyo3/tree/main/xtask#readme).
diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml
index 75216cde..531abd96 100644
--- a/.github/workflows/guide.yml
+++ b/.github/workflows/guide.yml
@@ -45,7 +45,7 @@ jobs:
mkdir target
mkdir -p gh-pages-build/internal
echo "
" > target/banner.html
- cargo +nightly pyo3_doc_internal
+ cargo xtask doc --internal
cp -r target/doc gh-pages-build/internal
env:
RUSTDOCFLAGS: "--cfg docsrs --Z unstable-options --document-hidden-items --html-before-content target/banner.html"
@@ -71,7 +71,7 @@ jobs:
# This adds the docs to gh-pages-build/doc
- name: Build the doc
run: |
- cargo +nightly pyo3_doc_scrape
+ cargo xtask doc
cp -r target/doc gh-pages-build/doc
echo "" > gh-pages-build/doc/index.html
diff --git a/.gitignore b/.gitignore
index 7098c348..9e87d2a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ guide/book/
extensions/stamps/
pip-wheel-metadata
valgrind-python.supp
+*.pyd
diff --git a/Contributing.md b/Contributing.md
index 4fc2c246..a48be238 100644
--- a/Contributing.md
+++ b/Contributing.md
@@ -48,7 +48,7 @@ There are some specific areas of focus where help is currently needed for the do
- Not all APIs had docs or examples when they were made. The goal is to have documentation on all PyO3 APIs ([#306](https://github.com/PyO3/pyo3/issues/306)). If you see an API lacking a doc, please write one and open a PR!
You can build the docs (including all features) with
-```cargo +nightly pyo3_doc_scrape```
+```cargo xtask doc --open```
#### Doctests
@@ -87,6 +87,10 @@ Tests run with all supported Python versions with the latest stable Rust compile
If you are adding a new feature, you should add it to the `full` feature in our *Cargo.toml** so that it is tested in CI.
+You can run these tests yourself with
+```cargo xtask ci```
+See [it's documentation](https://github.com/PyO3/pyo3/tree/main/xtask#readme)for more commands you can run.
+
## Python and Rust version support policy
PyO3 aims to keep sufficient compatibility to make packaging Python extensions built with PyO3 feasible on most common package managers.
diff --git a/pytests/tests/test_datetime.py b/pytests/tests/test_datetime.py
index 67d2da16..e70504e7 100644
--- a/pytests/tests/test_datetime.py
+++ b/pytests/tests/test_datetime.py
@@ -54,8 +54,9 @@ elif _pointer_size == 4:
else:
raise RuntimeError("unexpected pointer size: " + repr(_pointer_size))
IS_WINDOWS = sys.platform == "win32"
+
if IS_WINDOWS:
- MIN_DATETIME = pdt.datetime(1970, 1, 2, 0, 0)
+ MIN_DATETIME = pdt.datetime(1971, 1, 2, 0, 0)
if IS_32_BIT:
MAX_DATETIME = pdt.datetime(3001, 1, 19, 4, 59, 59)
else:
@@ -227,7 +228,7 @@ def test_datetime_typeerror():
@given(dt=st.datetimes(MIN_DATETIME, MAX_DATETIME))
-@example(dt=pdt.datetime(1970, 1, 2, 0, 0))
+@example(dt=pdt.datetime(1971, 1, 2, 0, 0))
def test_datetime_from_timestamp(dt):
if PYPY and dt < pdt.datetime(1900, 1, 1):
pytest.xfail("pdt.datetime.timestamp will raise on PyPy with dates before 1900")
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index f6008412..b3560ae1 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -3,9 +3,13 @@ name = "xtask"
version = "0.1.0"
edition = "2018"
+[[bin]]
+name = "xtask"
+
[dependencies]
anyhow = "1.0.51"
# Clap 3 requires MSRV 1.54
rustversion = "1.0"
structopt = { version = "0.3", default-features = false }
+clap = { version = "2" }
diff --git a/xtask/README.md b/xtask/README.md
new file mode 100644
index 00000000..68d078e5
--- /dev/null
+++ b/xtask/README.md
@@ -0,0 +1,23 @@
+## Commands to test PyO3.
+
+To run these commands, you should be in PyO3's root directory, and run (for example) `cargo xtask ci`.
+
+```
+USAGE:
+ xtask.exe
+
+FLAGS:
+ -h, --help Prints help information
+ -V, --version Prints version information
+
+SUBCOMMANDS:
+ ci Runs everything
+ clippy Runs `clippy`, denying all warnings
+ coverage Runs `cargo llvm-cov` for the PyO3 codebase
+ default Only runs the fast things (this is used if no command is specified)
+ doc Attempts to render the documentation
+ fmt Checks Rust and Python code formatting with `rustfmt` and `black`
+ help Prints this message or the help of the given subcommand(s)
+ test Runs various variations on `cargo test`
+ test-py Runs the tests in examples/ and pytests/
+```
\ No newline at end of file
diff --git a/xtask/src/cli.rs b/xtask/src/cli.rs
new file mode 100644
index 00000000..42b384ce
--- /dev/null
+++ b/xtask/src/cli.rs
@@ -0,0 +1,201 @@
+use crate::utils::*;
+use anyhow::{ensure, Result};
+use std::io;
+use std::process::{Command, Stdio};
+use std::time::Instant;
+use structopt::StructOpt;
+
+pub const MSRV: &str = "1.48";
+
+#[derive(StructOpt)]
+pub enum Subcommand {
+ /// Only runs the fast things (this is used if no command is specified)
+ Default,
+ /// Runs everything
+ Ci,
+ /// Checks Rust and Python code formatting with `rustfmt` and `black`
+ Fmt,
+ /// Runs `clippy`, denying all warnings.
+ Clippy,
+ /// Runs `cargo llvm-cov` for the PyO3 codebase.
+ Coverage(CoverageOpts),
+ /// Attempts to render the documentation.
+ Doc(DocOpts),
+ /// Runs various variations on `cargo test`
+ Test,
+ /// Runs the tests in examples/ and pytests/
+ TestPy,
+}
+
+impl Default for Subcommand {
+ fn default() -> Self {
+ Self::Default
+ }
+}
+
+#[derive(StructOpt, Default)]
+pub struct CoverageOpts {
+ /// Creates an lcov output instead of printing to the terminal.
+ #[structopt(long)]
+ pub output_lcov: Option,
+}
+
+#[derive(StructOpt)]
+pub struct DocOpts {
+ /// Whether to run the docs using nightly rustdoc
+ #[structopt(long)]
+ pub stable: bool,
+ /// Whether to open the docs after rendering.
+ #[structopt(long)]
+ pub open: bool,
+ /// Whether to show the private and hidden API.
+ #[structopt(long)]
+ pub internal: bool,
+}
+
+impl Default for DocOpts {
+ fn default() -> Self {
+ Self {
+ stable: true,
+ open: false,
+ internal: false,
+ }
+ }
+}
+
+impl Subcommand {
+ pub fn execute(self) -> Result<()> {
+ print_metadata()?;
+
+ let start = Instant::now();
+
+ match self {
+ Subcommand::Default => {
+ crate::fmt::rust::run()?;
+ crate::clippy::run()?;
+ crate::test::run()?;
+ crate::doc::run(DocOpts::default())?;
+ }
+ Subcommand::Ci => {
+ let installed = Installed::new()?;
+ crate::fmt::rust::run()?;
+ if installed.black {
+ crate::fmt::python::run()?;
+ } else {
+ Installed::warn_black()
+ };
+ crate::clippy::run()?;
+ crate::test::run()?;
+ crate::doc::run(DocOpts::default())?;
+ if installed.nox {
+ crate::pytests::run(None)?;
+ } else {
+ Installed::warn_nox()
+ };
+ crate::llvm_cov::run(CoverageOpts::default())?;
+ installed.assert()?
+ }
+
+ Subcommand::Doc(opts) => crate::doc::run(opts)?,
+ Subcommand::Fmt => {
+ crate::fmt::rust::run()?;
+ crate::fmt::python::run()?;
+ }
+ Subcommand::Clippy => crate::clippy::run()?,
+ Subcommand::Coverage(opts) => crate::llvm_cov::run(opts)?,
+ Subcommand::TestPy => crate::pytests::run(None)?,
+ Subcommand::Test => crate::test::run()?,
+ };
+
+ let dt = start.elapsed().as_secs();
+ let minutes = dt / 60;
+ let seconds = dt % 60;
+ println!("\nxtask finished in {}m {}s.", minutes, seconds);
+
+ Ok(())
+ }
+}
+
+pub fn run(command: &mut Command) -> Result<()> {
+ println!("Running: {}", format_command(command));
+
+ let output = command
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?
+ .wait_with_output()?;
+
+ ensure! {
+ output.status.success(),
+ "process did not run successfully ({exit}): {command}/n {out} {err}",
+ exit = match output.status.code() {
+ Some(code) => format!("exit code {}", code),
+ None => "terminated by signal".into(),
+ },
+ command = format_command(command),
+ out = String::from_utf8_lossy(&output.stdout),
+ err = String::from_utf8_lossy(&output.stderr)
+
+ };
+ Ok(())
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct Installed {
+ pub nox: bool,
+ pub black: bool,
+}
+
+impl Installed {
+ pub fn new() -> anyhow::Result {
+ Ok(Self {
+ nox: Self::nox()?,
+ black: Self::black()?,
+ })
+ }
+
+ pub fn nox() -> anyhow::Result {
+ let output = std::process::Command::new("nox").arg("--version").output();
+ match output {
+ Ok(_) => Ok(true),
+ Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
+ Err(other) => Err(other.into()),
+ }
+ }
+
+ pub fn warn_nox() {
+ eprintln!("Skipping: formatting Python code, because `nox` was not found");
+ }
+
+ pub fn black() -> anyhow::Result {
+ let output = std::process::Command::new("black")
+ .arg("--version")
+ .output();
+ match output {
+ Ok(_) => Ok(true),
+ Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
+ Err(other) => Err(other.into()),
+ }
+ }
+
+ pub fn warn_black() {
+ eprintln!("Skipping: Python code formatting, because `black` was not found.");
+ }
+
+ pub fn assert(&self) -> anyhow::Result<()> {
+ if self.nox && self.black {
+ Ok(())
+ } else {
+ let mut err =
+ String::from("\n\nxtask was unable to run all tests due to some missing programs:");
+ if !self.black {
+ err.push_str("\n`black` was not installed. (`pip install black`)");
+ }
+ if !self.nox {
+ err.push_str("\n`nox` was not installed. (`pip install nox`)");
+ }
+
+ Err(anyhow::anyhow!(err))
+ }
+ }
+}
diff --git a/xtask/src/clippy.rs b/xtask/src/clippy.rs
new file mode 100644
index 00000000..eec5f5fd
--- /dev/null
+++ b/xtask/src/clippy.rs
@@ -0,0 +1,25 @@
+use crate::cli;
+use std::process::Command;
+
+pub fn run() -> anyhow::Result<()> {
+ cli::run(
+ Command::new("cargo")
+ .arg("clippy")
+ .arg("--features=full")
+ .arg("--all-targets")
+ .arg("--workspace")
+ .arg("--")
+ .arg("-Dwarnings"),
+ )?;
+ cli::run(
+ Command::new("cargo")
+ .arg("clippy")
+ .arg("--all-targets")
+ .arg("--workspace")
+ .arg("--features=abi3,full")
+ .arg("--")
+ .arg("-Dwarnings"),
+ )?;
+
+ Ok(())
+}
diff --git a/xtask/src/doc.rs b/xtask/src/doc.rs
new file mode 100644
index 00000000..9fe49f30
--- /dev/null
+++ b/xtask/src/doc.rs
@@ -0,0 +1,47 @@
+use crate::cli;
+use crate::cli::DocOpts;
+use std::process::Command;
+//--cfg docsrs --Z unstable-options --document-hidden-items
+
+pub fn run(opts: DocOpts) -> anyhow::Result<()> {
+ let mut flags = Vec::new();
+
+ if !opts.stable {
+ flags.push("--cfg docsrs");
+ }
+ if opts.internal {
+ flags.push("--Z unstable-options");
+ flags.push("--document-hidden-items");
+ }
+ flags.push("-Dwarnings");
+
+ std::env::set_var("RUSTDOCFLAGS", flags.join(" "));
+ cli::run(
+ Command::new("cargo")
+ .args(if opts.stable { None } else { Some("+nightly") })
+ .arg("doc")
+ .arg("--lib")
+ .arg("--no-default-features")
+ .arg("--features=full")
+ .arg("--no-deps")
+ .arg("--workspace")
+ .args(if opts.internal {
+ &["--document-private-items"][..]
+ } else {
+ &["--exclude=pyo3-macros", "--exclude=pyo3-macros-backend"][..]
+ })
+ .args(if opts.stable {
+ &[][..]
+ } else {
+ &[
+ "-Z",
+ "unstable-options",
+ "-Z",
+ "rustdoc-scrape-examples=examples",
+ ]
+ })
+ .args(if opts.open { Some("--open") } else { None }),
+ )?;
+
+ Ok(())
+}
diff --git a/xtask/src/fmt.rs b/xtask/src/fmt.rs
new file mode 100644
index 00000000..8bc74524
--- /dev/null
+++ b/xtask/src/fmt.rs
@@ -0,0 +1,23 @@
+pub mod rust {
+ use crate::cli;
+ use std::process::Command;
+ pub fn run() -> anyhow::Result<()> {
+ cli::run(
+ Command::new("cargo")
+ .arg("fmt")
+ .arg("--all")
+ .arg("--")
+ .arg("--check"),
+ )?;
+ Ok(())
+ }
+}
+
+pub mod python {
+ use crate::cli;
+ use std::process::Command;
+ pub fn run() -> anyhow::Result<()> {
+ cli::run(Command::new("black").arg(".").arg("--check"))?;
+ Ok(())
+ }
+}
diff --git a/xtask/src/llvm_cov.rs b/xtask/src/llvm_cov.rs
new file mode 100644
index 00000000..50ea70e6
--- /dev/null
+++ b/xtask/src/llvm_cov.rs
@@ -0,0 +1,100 @@
+use crate::cli;
+use crate::cli::CoverageOpts;
+use crate::utils::*;
+use anyhow::{Context, Result};
+use std::{collections::HashMap, process::Command};
+
+/// Runs `cargo llvm-cov` for the PyO3 codebase.
+pub fn run(opts: CoverageOpts) -> Result<()> {
+ let env = get_coverage_env()?;
+
+ cli::run(llvm_cov_command(&["clean", "--workspace"]).envs(&env))?;
+
+ cli::run(
+ Command::new("cargo")
+ .args(&["test", "--manifest-path", "pyo3-build-config/Cargo.toml"])
+ .envs(&env),
+ )?;
+ cli::run(
+ Command::new("cargo")
+ .args(&["test", "--manifest-path", "pyo3-macros-backend/Cargo.toml"])
+ .envs(&env),
+ )?;
+ cli::run(
+ Command::new("cargo")
+ .args(&["test", "--manifest-path", "pyo3-macros/Cargo.toml"])
+ .envs(&env),
+ )?;
+
+ cli::run(Command::new("cargo").arg("test").envs(&env))?;
+ cli::run(
+ Command::new("cargo")
+ .args(&["test", "--features", "abi3"])
+ .envs(&env),
+ )?;
+ cli::run(
+ Command::new("cargo")
+ .args(&["test", "--features", "full"])
+ .envs(&env),
+ )?;
+ cli::run(
+ Command::new("cargo")
+ .args(&["test", "--features", "abi3 full"])
+ .envs(&env),
+ )?;
+
+ crate::pytests::run(&env)?;
+
+ match opts.output_lcov {
+ Some(path) => {
+ cli::run(llvm_cov_command(&["--no-run", "--lcov", "--output-path", &path]).envs(&env))?
+ }
+ None => cli::run(llvm_cov_command(&["--no-run", "--summary-only"]).envs(&env))?,
+ }
+
+ Ok(())
+}
+
+fn llvm_cov_command(args: &[&str]) -> Command {
+ let mut command = Command::new("cargo");
+ command
+ .args(&[
+ "llvm-cov",
+ "--package=pyo3",
+ "--package=pyo3-build-config",
+ "--package=pyo3-macros-backend",
+ "--package=pyo3-macros",
+ "--package=pyo3-ffi",
+ ])
+ .args(args);
+ command
+}
+
+fn get_coverage_env() -> Result> {
+ let mut env = HashMap::new();
+
+ let output = String::from_utf8(llvm_cov_command(&["show-env"]).output()?.stdout)?;
+
+ for line in output.trim().split('\n') {
+ let (key, value) = split_once(line, '=')
+ .context("expected '=' in each line of output from llvm-cov show-env")?;
+ env.insert(key.to_owned(), value.trim_matches('"').to_owned());
+ }
+
+ // Ensure that examples/ and pytests/ all build to the correct target directory to collect
+ // coverage artifacts.
+ env.insert(
+ "CARGO_TARGET_DIR".to_owned(),
+ env.get("CARGO_LLVM_COV_TARGET_DIR").unwrap().to_owned(),
+ );
+
+ // Coverage only works on nightly.
+ let rustc_version =
+ String::from_utf8(get_output(Command::new("rustc").arg("--version"))?.stdout)
+ .context("failed to parse rust version as utf8")?;
+ if !rustc_version.contains("nightly") {
+ env.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned());
+ }
+
+ Ok(env)
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index cdbfbf14..ab7afafa 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1,196 +1,24 @@
-use anyhow::{ensure, Context, Result};
-use std::{collections::HashMap, path::Path, process::Command};
+use clap::ErrorKind::MissingArgumentOrSubcommand;
use structopt::StructOpt;
-#[derive(StructOpt)]
-enum Subcommand {
- /// Runs `cargo llvm-cov` for the PyO3 codebase.
- Coverage(CoverageOpts),
- /// Runs tests in examples/ and pytests/
- TestPy,
-}
+pub mod cli;
+pub mod clippy;
+pub mod doc;
+pub mod fmt;
+pub mod llvm_cov;
+pub mod pytests;
+pub mod test;
+pub mod utils;
-#[derive(StructOpt)]
-struct CoverageOpts {
- /// Creates an lcov output instead of printing to the terminal.
- #[structopt(long)]
- output_lcov: Option,
-}
+fn main() -> anyhow::Result<()> {
+ // Avoid spewing backtraces all over the command line
+ // For some reason this is automatically enabled on nightly compilers...
+ std::env::set_var("RUST_LIB_BACKTRACE", "0");
-impl Subcommand {
- fn execute(self) -> Result<()> {
- match self {
- Subcommand::Coverage(opts) => subcommand_coverage(opts),
- Subcommand::TestPy => run_python_tests(None),
- }
- }
-}
-
-fn main() -> Result<()> {
- Subcommand::from_args().execute()
-}
-
-/// Runs `cargo llvm-cov` for the PyO3 codebase.
-fn subcommand_coverage(opts: CoverageOpts) -> Result<()> {
- let env = get_coverage_env()?;
-
- run(llvm_cov_command(&["clean", "--workspace"]).envs(&env))?;
-
- run(Command::new("cargo")
- .args(&["test", "--manifest-path", "pyo3-build-config/Cargo.toml"])
- .envs(&env))?;
- run(Command::new("cargo")
- .args(&["test", "--manifest-path", "pyo3-macros-backend/Cargo.toml"])
- .envs(&env))?;
- run(Command::new("cargo")
- .args(&["test", "--manifest-path", "pyo3-macros/Cargo.toml"])
- .envs(&env))?;
-
- run(Command::new("cargo").arg("test").envs(&env))?;
- run(Command::new("cargo")
- .args(&["test", "--features", "abi3"])
- .envs(&env))?;
- run(Command::new("cargo")
- .args(&["test", "--features", "full"])
- .envs(&env))?;
- run(Command::new("cargo")
- .args(&["test", "--features", "abi3 full"])
- .envs(&env))?;
-
- run_python_tests(&env)?;
-
- match opts.output_lcov {
- Some(path) => {
- run(llvm_cov_command(&["--no-run", "--lcov", "--output-path", &path]).envs(&env))?
- }
- None => run(llvm_cov_command(&["--no-run", "--summary-only"]).envs(&env))?,
- }
-
- Ok(())
-}
-
-fn run(command: &mut Command) -> Result<()> {
- println!("running: {}", format_command(command));
- let status = command.spawn()?.wait()?;
- ensure! {
- status.success(),
- "process did not run successfully ({exit}): {command}",
- exit = match status.code() {
- Some(code) => format!("exit code {}", code),
- None => "terminated by signal".into(),
- },
- command = format_command(command),
- };
- Ok(())
-}
-
-fn get_output(command: &mut Command) -> Result {
- let output = command.output()?;
- ensure! {
- output.status.success(),
- "process did not run successfully ({exit}): {command}",
- exit = match output.status.code() {
- Some(code) => format!("exit code {}", code),
- None => "terminated by signal".into(),
- },
- command = format_command(command),
- };
- Ok(output)
-}
-
-fn llvm_cov_command(args: &[&str]) -> Command {
- let mut command = Command::new("cargo");
- command
- .args(&[
- "llvm-cov",
- "--package=pyo3",
- "--package=pyo3-build-config",
- "--package=pyo3-macros-backend",
- "--package=pyo3-macros",
- "--package=pyo3-ffi",
- ])
- .args(args);
- command
-}
-
-fn run_python_tests<'a>(
- env: impl IntoIterator- + Copy,
-) -> Result<()> {
- run(Command::new("nox")
- .arg("--non-interactive")
- .arg("-f")
- .arg(Path::new("pytests").join("noxfile.py"))
- .envs(env))?;
-
- for entry in std::fs::read_dir("examples")? {
- let path = entry?.path();
- if path.is_dir() && path.join("noxfile.py").exists() {
- run(Command::new("nox")
- .arg("--non-interactive")
- .arg("-f")
- .arg(path.join("noxfile.py"))
- .envs(env))?;
- }
+ match cli::Subcommand::from_args_safe() {
+ Ok(c) => c.execute()?,
+ Err(e) if e.kind == MissingArgumentOrSubcommand => cli::Subcommand::default().execute()?,
+ Err(e) => return Err(e.into()),
}
Ok(())
}
-
-fn get_coverage_env() -> Result> {
- let mut env = HashMap::new();
-
- let output = String::from_utf8(llvm_cov_command(&["show-env"]).output()?.stdout)?;
-
- for line in output.trim().split('\n') {
- let (key, value) = split_once(line, '=')
- .context("expected '=' in each line of output from llvm-cov show-env")?;
- env.insert(key.to_owned(), value.trim_matches('"').to_owned());
- }
-
- // Ensure that examples/ and pytests/ all build to the correct target directory to collect
- // coverage artifacts.
- env.insert(
- "CARGO_TARGET_DIR".to_owned(),
- env.get("CARGO_LLVM_COV_TARGET_DIR").unwrap().to_owned(),
- );
-
- // Coverage only works on nightly.
- let rustc_version =
- String::from_utf8(get_output(Command::new("rustc").arg("--version"))?.stdout)
- .context("failed to parse rust version as utf8")?;
- if !rustc_version.contains("nightly") {
- env.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned());
- }
-
- Ok(env)
-}
-
-// Replacement for str.split_once() on Rust older than 1.52
-#[rustversion::before(1.52)]
-fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
- let mut iter = s.splitn(2, pat);
- Some((iter.next()?, iter.next()?))
-}
-
-#[rustversion::since(1.52)]
-fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
- s.split_once(pat)
-}
-
-#[rustversion::since(1.57)]
-fn format_command(command: &Command) -> String {
- let mut buf = String::new();
- buf.push('`');
- buf.push_str(&command.get_program().to_string_lossy());
- for arg in command.get_args() {
- buf.push(' ');
- buf.push_str(&arg.to_string_lossy());
- }
- buf.push('`');
- buf
-}
-
-#[rustversion::before(1.57)]
-fn format_command(command: &Command) -> String {
- // Debug impl isn't as nice as the above, but will do on < 1.57
- format!("{:?}", command)
-}
diff --git a/xtask/src/pytests.rs b/xtask/src/pytests.rs
new file mode 100644
index 00000000..78744c69
--- /dev/null
+++ b/xtask/src/pytests.rs
@@ -0,0 +1,27 @@
+use crate::cli;
+use anyhow::Result;
+use std::{path::Path, process::Command};
+
+pub fn run<'a>(env: impl IntoIterator
- + Copy) -> Result<()> {
+ cli::run(
+ Command::new("nox")
+ .arg("--non-interactive")
+ .arg("-f")
+ .arg(Path::new("pytests").join("noxfile.py"))
+ .envs(env),
+ )?;
+
+ for entry in std::fs::read_dir("examples")? {
+ let path = entry?.path();
+ if path.is_dir() && path.join("noxfile.py").exists() {
+ cli::run(
+ Command::new("nox")
+ .arg("--non-interactive")
+ .arg("-f")
+ .arg(path.join("noxfile.py"))
+ .envs(env),
+ )?;
+ }
+ }
+ Ok(())
+}
diff --git a/xtask/src/test.rs b/xtask/src/test.rs
new file mode 100644
index 00000000..c383140a
--- /dev/null
+++ b/xtask/src/test.rs
@@ -0,0 +1,73 @@
+use crate::cli::{self, MSRV};
+use std::process::Command;
+
+pub fn run() -> anyhow::Result<()> {
+ cli::run(
+ Command::new("cargo")
+ .arg("test")
+ .arg("--lib")
+ .arg("--no-default-features")
+ .arg("--tests")
+ .arg("--quiet"),
+ )?;
+
+ cli::run(
+ Command::new("cargo")
+ .arg("test")
+ .arg("--no-default-features")
+ .arg("--features=full")
+ .arg("--quiet"),
+ )?;
+
+ cli::run(
+ Command::new("cargo")
+ .arg("test")
+ .arg("--no-default-features")
+ .arg("--features=abi3,full")
+ .arg("--quiet"),
+ )?;
+
+ // If the MSRV toolchain is not installed, this will install it
+ cli::run(
+ Command::new("rustup")
+ .arg("toolchain")
+ .arg("install")
+ .arg(MSRV),
+ )?;
+
+ // Test MSRV
+ cli::run(
+ Command::new("cargo")
+ .arg(format!("+{}", MSRV))
+ .arg("test")
+ .arg("--no-default-features")
+ .arg("--features=full,auto-initialize")
+ .arg("--quiet"),
+ )?;
+
+ cli::run(
+ Command::new("cargo")
+ .arg("+nightly")
+ .arg("test")
+ .arg("--no-default-features")
+ .arg("--features=full,nightly")
+ .arg("--quiet"),
+ )?;
+
+ cli::run(
+ Command::new("cargo")
+ .arg("test")
+ .arg("--manifest-path=pyo3-ffi/Cargo.toml")
+ .arg("--quiet"),
+ )?;
+
+ cli::run(
+ Command::new("cargo")
+ .arg("test")
+ .arg("--no-default-features")
+ .arg("--manifest-path=pyo3-build-config/Cargo.toml")
+ .arg("--quiet"),
+ )?;
+
+ Ok(())
+}
diff --git a/xtask/src/utils.rs b/xtask/src/utils.rs
new file mode 100644
index 00000000..045697e7
--- /dev/null
+++ b/xtask/src/utils.rs
@@ -0,0 +1,65 @@
+use anyhow::ensure;
+use std::process::Command;
+
+// Replacement for str.split_once() on Rust older than 1.52
+#[rustversion::before(1.52)]
+pub fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
+ let mut iter = s.splitn(2, pat);
+ Some((iter.next()?, iter.next()?))
+}
+
+#[rustversion::since(1.52)]
+pub fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
+ s.split_once(pat)
+}
+
+#[rustversion::since(1.57)]
+pub fn format_command(command: &Command) -> String {
+ let mut buf = String::new();
+ buf.push('`');
+ buf.push_str(&command.get_program().to_string_lossy());
+ for arg in command.get_args() {
+ buf.push(' ');
+ buf.push_str(&arg.to_string_lossy());
+ }
+ buf.push('`');
+ buf
+}
+
+#[rustversion::before(1.57)]
+pub fn format_command(command: &Command) -> String {
+ // Debug impl isn't as nice as the above, but will do on < 1.57
+ format!("{:?}", command)
+}
+
+pub fn get_output(command: &mut Command) -> anyhow::Result {
+ let output = command.output()?;
+ ensure! {
+ output.status.success(),
+ "process did not run successfully ({exit}): {command}",
+ exit = match output.status.code() {
+ Some(code) => format!("exit code {}", code),
+ None => "terminated by signal".into(),
+ },
+ command = format_command(command),
+ };
+ Ok(output)
+}
+
+pub fn print_metadata() -> anyhow::Result<()> {
+ let rustc_output = std::process::Command::new("rustc")
+ .arg("--version")
+ .arg("--verbose")
+ .output()?;
+ let rustc_version = core::str::from_utf8(&rustc_output.stdout).unwrap();
+ println!("Metadata: \n\n{}", rustc_version);
+
+ let py_output = std::process::Command::new("python")
+ .arg("--version")
+ .arg("-V")
+ .output()?;
+ let py_version = core::str::from_utf8(&py_output.stdout).unwrap();
+ println!("{}", py_version);
+
+ Ok(())
+}
From 69655454c146cd7c965a874fca9907a316636651 Mon Sep 17 00:00:00 2001
From: Alex Gaynor
Date: Sat, 19 Mar 2022 11:46:40 -0400
Subject: [PATCH 2/9] Added an as_bytes method for Py
This allows for obtaining a slice that's not lexically bound to the GIL which can be helpful to avoid copying.
---
CHANGELOG.md | 4 ++++
src/types/bytes.rs | 18 ++++++++++++++++++
tests/test_bytes.rs | 15 +++++++++++++++
3 files changed, 37 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bded739..9615c5a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Panic during compilation when `PYO3_CROSS_LIB_DIR` is set for some host/target combinations. [#2232](https://github.com/PyO3/pyo3/pull/2232)
+### Added
+
+- Added `as_bytes` on `Py`. [#2235](https://github.com/PyO3/pyo3/pull/2235)
+
## [0.16.2] - 2022-03-15
### Packaging
diff --git a/src/types/bytes.rs b/src/types/bytes.rs
index e733582b..96a49960 100644
--- a/src/types/bytes.rs
+++ b/src/types/bytes.rs
@@ -97,6 +97,24 @@ impl PyBytes {
}
}
+impl Py {
+ /// Gets the Python bytes as a byte slice. Because Python bytes are
+ /// immutable, the result may be used for as long as the reference to
+ /// `self` is held, including when the GIL is released.
+ pub fn as_bytes<'a>(&'a self, _py: Python<'_>) -> &'a [u8] {
+ // py is required here because `PyBytes_AsString` and `PyBytes_Size`
+ // can both technically raise exceptions which require the GIL to be
+ // held. The only circumstance in which they raise is if the value
+ // isn't really a `PyBytes`, but better safe than sorry.
+ unsafe {
+ let buffer = ffi::PyBytes_AsString(self.as_ptr()) as *const u8;
+ let length = ffi::PyBytes_Size(self.as_ptr()) as usize;
+ debug_assert!(!buffer.is_null());
+ std::slice::from_raw_parts(buffer, length)
+ }
+ }
+}
+
/// This is the same way [Vec] is indexed.
impl> Index for PyBytes {
type Output = I::Output;
diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs
index 710677dd..36ae41e4 100644
--- a/tests/test_bytes.rs
+++ b/tests/test_bytes.rs
@@ -41,3 +41,18 @@ fn test_bytearray_vec_conversion() {
let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap();
py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'");
}
+
+#[test]
+fn test_py_as_bytes() {
+ let pyobj: pyo3::Py;
+ let data: &[u8];
+
+ {
+ let gil = Python::acquire_gil();
+ let py = gil.python();
+ pyobj = pyo3::types::PyBytes::new(py, b"abc").into_py(py);
+ data = pyobj.as_bytes(py);
+ }
+
+ assert_eq!(data, b"abc");
+}
From 5cc3ce99f144be3331f7600d3665958cff264410 Mon Sep 17 00:00:00 2001
From: David Hewitt <1939362+davidhewitt@users.noreply.github.com>
Date: Fri, 18 Mar 2022 14:58:44 +0000
Subject: [PATCH 3/9] pyclass: unify pyclass with its pyo3 arguments
---
pyo3-macros-backend/src/attributes.rs | 122 +++++---
pyo3-macros-backend/src/frompyobject.rs | 10 +-
pyo3-macros-backend/src/konst.rs | 4 +-
pyo3-macros-backend/src/method.rs | 4 +-
pyo3-macros-backend/src/module.rs | 4 +-
pyo3-macros-backend/src/params.rs | 4 +-
pyo3-macros-backend/src/pyclass.rs | 372 ++++++++++--------------
pyo3-macros-backend/src/pyfunction.rs | 10 +-
pyo3-macros-backend/src/pyimpl.rs | 2 +-
pyo3-macros-backend/src/pymethod.rs | 2 +-
pyo3-macros-backend/src/utils.rs | 12 +-
pyo3-macros/src/lib.rs | 31 +-
tests/ui/invalid_property_args.stderr | 8 +-
tests/ui/invalid_pyclass_args.stderr | 14 +-
tests/ui/invalid_pyclass_enum.rs | 8 +-
tests/ui/invalid_pyclass_enum.stderr | 4 +-
tests/ui/invalid_pymethod_names.stderr | 4 +-
17 files changed, 292 insertions(+), 323 deletions(-)
diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs
index e0566076..d6801d32 100644
--- a/pyo3-macros-backend/src/attributes.rs
+++ b/pyo3-macros-backend/src/attributes.rs
@@ -1,77 +1,107 @@
+use proc_macro2::TokenStream;
+use quote::ToTokens;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
+ spanned::Spanned,
token::Comma,
- Attribute, ExprPath, Ident, LitStr, Path, Result, Token,
+ Attribute, Expr, ExprPath, Ident, LitStr, Path, Result, Token,
};
pub mod kw {
syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute);
+ syn::custom_keyword!(dict);
+ syn::custom_keyword!(extends);
+ syn::custom_keyword!(freelist);
syn::custom_keyword!(from_py_with);
+ syn::custom_keyword!(gc);
syn::custom_keyword!(get);
syn::custom_keyword!(item);
- syn::custom_keyword!(pass_module);
+ syn::custom_keyword!(module);
syn::custom_keyword!(name);
+ syn::custom_keyword!(pass_module);
syn::custom_keyword!(set);
syn::custom_keyword!(signature);
+ syn::custom_keyword!(subclass);
syn::custom_keyword!(text_signature);
syn::custom_keyword!(transparent);
+ syn::custom_keyword!(unsendable);
+ syn::custom_keyword!(weakref);
}
-#[derive(Clone, Debug, PartialEq)]
-pub struct FromPyWithAttribute(pub ExprPath);
+#[derive(Clone, Debug)]
+pub struct KeywordAttribute {
+ pub kw: K,
+ pub value: V,
+}
-impl Parse for FromPyWithAttribute {
+/// A helper type which parses the inner type via a literal string
+/// e.g. LitStrValue -> parses "some::path" in quotes.
+#[derive(Clone, Debug, PartialEq)]
+pub struct LitStrValue(pub T);
+
+impl Parse for LitStrValue {
fn parse(input: ParseStream) -> Result {
- let _: kw::from_py_with = input.parse()?;
- let _: Token![=] = input.parse()?;
- let string_literal: LitStr = input.parse()?;
- string_literal.parse().map(FromPyWithAttribute)
+ let lit_str: LitStr = input.parse()?;
+ lit_str.parse().map(LitStrValue)
}
}
-#[derive(Clone, Debug, PartialEq)]
-pub struct NameAttribute(pub Ident);
-
-impl Parse for NameAttribute {
- fn parse(input: ParseStream) -> Result {
- let _: kw::name = input.parse()?;
- let _: Token![=] = input.parse()?;
- let string_literal: LitStr = input.parse()?;
- string_literal.parse().map(NameAttribute)
+impl ToTokens for LitStrValue {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ self.0.to_tokens(tokens)
}
}
+/// A helper type which parses a name via a literal string
+#[derive(Clone, Debug, PartialEq)]
+pub struct NameLitStr(pub Ident);
+
+impl Parse for NameLitStr {
+ fn parse(input: ParseStream) -> Result {
+ let string_literal: LitStr = input.parse()?;
+ if let Ok(ident) = string_literal.parse() {
+ Ok(NameLitStr(ident))
+ } else {
+ bail_spanned!(string_literal.span() => "expected a single identifier in double quotes")
+ }
+ }
+}
+
+impl ToTokens for NameLitStr {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ self.0.to_tokens(tokens)
+ }
+}
+
+pub type ExtendsAttribute = KeywordAttribute;
+pub type FreelistAttribute = KeywordAttribute>;
+pub type ModuleAttribute = KeywordAttribute;
+pub type NameAttribute = KeywordAttribute;
+pub type TextSignatureAttribute = KeywordAttribute;
+
+impl Parse for KeywordAttribute {
+ fn parse(input: ParseStream) -> Result {
+ let kw: K = input.parse()?;
+ let _: Token![=] = input.parse()?;
+ let value = input.parse()?;
+ Ok(KeywordAttribute { kw, value })
+ }
+}
+
+impl ToTokens for KeywordAttribute {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ self.kw.to_tokens(tokens);
+ Token![=](self.kw.span()).to_tokens(tokens);
+ self.value.to_tokens(tokens);
+ }
+}
+
+pub type FromPyWithAttribute = KeywordAttribute>;
+
/// For specifying the path to the pyo3 crate.
-#[derive(Clone, Debug, PartialEq)]
-pub struct CrateAttribute(pub Path);
-
-impl Parse for CrateAttribute {
- fn parse(input: ParseStream) -> Result {
- let _: Token![crate] = input.parse()?;
- let _: Token![=] = input.parse()?;
- let string_literal: LitStr = input.parse()?;
- string_literal.parse().map(CrateAttribute)
- }
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub struct TextSignatureAttribute {
- pub kw: kw::text_signature,
- pub eq_token: Token![=],
- pub lit: LitStr,
-}
-
-impl Parse for TextSignatureAttribute {
- fn parse(input: ParseStream) -> Result {
- Ok(TextSignatureAttribute {
- kw: input.parse()?,
- eq_token: input.parse()?,
- lit: input.parse()?,
- })
- }
-}
+pub type CrateAttribute = KeywordAttribute>;
pub fn get_pyo3_options(attr: &syn::Attribute) -> Result