Skip to content
Open
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: 2 additions & 0 deletions pyo3-introspection/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ serde_json = "1"

[dev-dependencies]
tempfile = "3.12.0"
console = "0.16.2"
similar = "2.7.0"

[lints]
workspace = true
38 changes: 31 additions & 7 deletions pyo3-introspection/tests/test.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I think this makes the development process easier for us, happy to add these depedencies.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use anyhow::{ensure, Result};
use pyo3_introspection::{introspect_cdylib, module_stub_files};
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use tempfile::NamedTempFile;

#[test]
fn pytests_stubs() -> Result<()> {
// We run the introspection
Expand Down Expand Up @@ -49,12 +49,36 @@ fn pytests_stubs() -> Result<()> {
let actual_file_content = format_with_ruff(actual_file_content)?;

// We normalize line jumps for compatibility with Windows
assert_eq!(
expected_file_content.replace('\r', ""),
actual_file_content.replace('\r', ""),
"The content of file {} is different",
file_name.display()
)
let expected_file_content_fixed = expected_file_content.replace('\r', "");
let actual_file_content_fixed = actual_file_content.replace('\r', "");
let diff =
similar::TextDiff::from_lines(&expected_file_content_fixed, &actual_file_content_fixed);
if actual_file_content_fixed != expected_file_content_fixed {
let mut buffer = String::new();
writeln!(
&mut buffer,
"The stub file {} differs from the expected one. See the diff below.",
file_name.display()
)?;
writeln!(&mut buffer, "============================")?;
writeln!(&mut buffer, "============================")?;
for op in diff.ops() {
for change in diff.iter_changes(op) {
let (sign, style) = match change.tag() {
similar::ChangeTag::Delete => ("-", console::Style::new().red()),
similar::ChangeTag::Insert => ("+", console::Style::new().green()),
similar::ChangeTag::Equal => (" ", console::Style::new()),
};
write!(
&mut buffer,
"{}{}",
style.apply_to(sign).bold(),
style.apply_to(change)
)?;
}
}
panic!("{}", buffer);
}
}

Ok(())
Expand Down
21 changes: 8 additions & 13 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,15 @@ impl FnType {

pub fn signature_attribute_allowed(&self) -> bool {
match self {
FnType::Fn(_) | FnType::FnStatic | FnType::FnClass(_) | FnType::FnModule(_) => true,
// Getter, Setter and Deleter and ClassAttribute all have fixed signatures (either take 0 or 1
FnType::Setter(_)
| FnType::Getter(_)
| FnType::Fn(_)
| FnType::FnStatic
| FnType::FnClass(_)
| FnType::FnModule(_) => true,
// Deleter and ClassAttribute all have fixed signatures (either take 0 or 1
// arguments) so cannot have a `signature = (...)` attribute.
FnType::Getter(_) | FnType::Setter(_) | FnType::Deleter(_) | FnType::ClassAttribute => {
false
}
FnType::Deleter(_) | FnType::ClassAttribute => false,
}
}

Expand Down Expand Up @@ -1090,14 +1093,6 @@ fn ensure_signatures_on_valid_method(
) -> syn::Result<()> {
if let Some(signature) = signature {
match fn_type {
FnType::Getter(_) => {
debug_assert!(!fn_type.signature_attribute_allowed());
bail_spanned!(signature.kw.span() => "`signature` not allowed with `getter`")
}
FnType::Setter(_) => {
debug_assert!(!fn_type.signature_attribute_allowed());
bail_spanned!(signature.kw.span() => "`signature` not allowed with `setter`")
}
FnType::Deleter(_) => {
debug_assert!(!fn_type.signature_attribute_allowed());
bail_spanned!(signature.kw.span() => "`signature` not allowed with `deleter`")
Expand Down
37 changes: 35 additions & 2 deletions pytests/src/pyclasses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,39 @@ fn map_a_class(cls: AClass) -> AClass {
cls
}

#[pyclass]
struct ClassWithCustomGetterSetterSignature {
foo: usize,
bar: usize,
}
#[pymethods]
Comment on lines +207 to +212
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[pyclass]
struct ClassWithCustomGetterSetterSignature {
foo: usize,
bar: usize,
}
#[pymethods]
#[pyclass]
#[cfg(feature = "experimental-inspect")]
struct ClassWithCustomGetterSetterSignature {
foo: usize,
bar: usize,
}
#[cfg(feature = "experimental-inspect")]
#[pymethods]

impl ClassWithCustomGetterSetterSignature {
#[new]
fn new() -> Self {
Self { foo: 0, bar: 10 }
}
#[getter]
#[pyo3(signature = () -> "int")]
fn get_foo(&self) -> usize {
self.foo
}

#[setter]
#[pyo3(signature = (value:"int"))]
fn set_foo(&mut self, value: usize) {
self.foo = value;
}

#[getter]
fn get_bar(&self) -> usize {
self.bar
}
#[setter]
fn set_bar(&mut self, value: usize) {
self.bar = value;
}
}

#[pymodule]
pub mod pyclasses {
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
Expand All @@ -214,7 +247,7 @@ pub mod pyclasses {
use super::SubClassWithInit;
#[pymodule_export]
use super::{
map_a_class, AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass,
PlainObject, PyClassIter, PyClassThreadIter,
map_a_class, AssertingBaseClass, ClassWithCustomGetterSetterSignature, ClassWithDecorators,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to move this to a separate #[cfg(feature = "experimental-inspect")] export for the ClassWithCustomGetterSetterSignature.

ClassWithoutConstructor, EmptyClass, PlainObject, PyClassIter, PyClassThreadIter,
};
}
14 changes: 14 additions & 0 deletions pytests/stubs/pyclasses.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
from _typeshed import Incomplete
from typing import final



class AssertingBaseClass:
def __new__(cls, /, expected_type: type) -> AssertingBaseClass: ...

@final
class ClassWithCustomGetterSetterSignature:
def __new__(cls, /) -> ClassWithCustomGetterSetterSignature: ...
@property
def bar(self, /) -> int: ...
@bar.setter
def bar(self, /, value: int) -> None: ...
@property
def foo(self, /) -> "int": ...
@foo.setter
def foo(self, /, value: "int") -> None: ...

@final
class ClassWithDecorators:
def __new__(cls, /) -> ClassWithDecorators: ...
Expand Down
2 changes: 2 additions & 0 deletions tests/test_compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,6 @@ fn test_compile_errors() {
t.pass("tests/ui/pyclass_probe.rs");
t.compile_fail("tests/ui/invalid_pyfunction_warn.rs");
t.compile_fail("tests/ui/invalid_pymethods_warn.rs");
#[cfg(feature = "experimental-inspect")]
t.compile_fail("tests/ui/invalid_getter_setter_signatures.rs");
}
59 changes: 59 additions & 0 deletions tests/ui/invalid_getter_setter_signatures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use pyo3::prelude::*;

#[pyclass]
struct ClassWithBadGetterSignature {
foo: usize,
bar: usize,
}
#[pymethods]
impl ClassWithBadGetterSignature {
#[getter]
#[pyo3(signature = (extra_arg:"int"))]
fn get_foo(&self) -> usize {
self.foo
}
}

#[pyclass]
struct ClassWithMismatchedSetterSignature {
foo: usize,
bar: usize,
}
#[pymethods]
impl ClassWithMismatchedSetterSignature {
#[getter]
#[pyo3(signature = (extra_arg:"int"))]
fn set_foo(&mut self, value: usize) {
self.foo = value;
}
}

#[pyclass]
struct ClassWithMissingSetterSignature {
foo: usize,
bar: usize,
}
#[pymethods]
impl ClassWithMissingSetterSignature {
#[getter]
#[pyo3(signature = ())]
fn set_foo(&mut self, value: usize) {
self.foo = value;
}
}

#[pyclass]
struct ClassWithExtraSetterSignature {
foo: usize,
bar: usize,
}
#[pymethods]
impl ClassWithExtraSetterSignature {
#[getter]
#[pyo3(signature = (value:"int", extra_arg:"int"))]
fn set_foo(&mut self, value: usize) {
self.foo = value;
}
}

fn main() {}
23 changes: 23 additions & 0 deletions tests/ui/invalid_getter_setter_signatures.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
error: signature entry does not have a corresponding function argument
--> tests/ui/invalid_getter_setter_signatures.rs:11:25
|
11 | #[pyo3(signature = (extra_arg:"int"))]
| ^^^^^^^^^

error: expected argument from function definition `value` but got argument `extra_arg`
--> tests/ui/invalid_getter_setter_signatures.rs:25:25
|
25 | #[pyo3(signature = (extra_arg:"int"))]
| ^^^^^^^^^

error: missing signature entry for argument `value`
--> tests/ui/invalid_getter_setter_signatures.rs:39:12
|
39 | #[pyo3(signature = ())]
| ^^^^^^^^^

error: signature entry does not have a corresponding function argument
--> tests/ui/invalid_getter_setter_signatures.rs:53:38
|
53 | #[pyo3(signature = (value:"int", extra_arg:"int"))]
| ^^^^^^^^^
Loading