-
Notifications
You must be signed in to change notification settings - Fork 763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for wrapping attributes in #[cfg_attr(feature = "pyo3", …)]
#2786
Conversation
Does this fix #780 ? (It was closed by the submitter, not fixed.) |
yep! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the new test file, that is a lot of cfg_attr
...
Wild idea: how about a macro that can be applied to a whole module/scope and removes the pyo3 related attributes? It can then be added with cfg_attr(not(feature = "pyo3"), ...)
if let Some(options) = get_pyo3_options(attr)? { | ||
let mut new_attrs = Vec::new(); | ||
|
||
for mut attr in attrs.drain(..) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also do
for mut attr in std::mem::take(attrs)
and then push to attrs
, saving line 149 and 177.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it easier to read when we have a distinct old-attr-drain and new attr building; iterating and editing/deleting at the same time is unfortunately difficult to write straightforward
I've thought about that too: We could have a |
It would have to be a separate crate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very cool, thanks for implementing!
If I understand correctly, this works by assuming that the top-level macro was also wrapped in #[cfg_attr(feature = "pyo3", pyclass)]
(for example).
There is some interesting edge cases from this, e.g. I think the following code will work even in crates which don't define a "pyo3" feature:
#[pyclass]
struct Foo;
#[pymethods]
impl Foo {
#[cfg_attr(feature = "pyo3", new)]
fn new() -> Self { Self }
}
... because the current assumption seems to be that if the macro is running, the top-level macro has already passed the cfg_attr
gate.
# This makes `#[cfg_attr(feature = "pyo3", pyclass)]` in our own tests work, it has no other function | ||
pyo3 = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder... should we move this to the pytests
crate or something similar, so that we don't need to expose a no-op feature to the users?
To avoid other code changes, you might also just be able to hack it by adding RUSTFLAGS=--cfg "feature=pyo3"
, which is all cargo
really does under the hood I think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so like moving the ui test to pytests/src
? The RUSTFLAGS
would also do, but imho it'd be nice if cargo test
would continue to work.
I've also been thinking about using a different feature name for the tests, but i couldn't come up with a good design for that either, especially for cases such as the guide doc test
if let Ok(mut meta) = attr.parse_meta() { | ||
if handle_cfg_feature_pyo3(&mut attr, &mut meta, parse_attr)? { | ||
continue; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good to check if attr.path.is_ident("cfg_attr")
before calling parse_meta
, to avoid a lot of unnecessary parsing work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is parsing Meta
expensive? This is the only location where we do it in this order, in the other two locations we also need meta
for the normal parsing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I've now written a benchmark in #2794
It's based against main, what I see on my machine is that the cost per attribute (for a very basic attribute) goes from about 250ns to 1us. Most of that overhead is removed again by just adding this check.
So sure, while it's not much, maybe in a bigger codebase with lots of attributes this could eventually add up to a second or so. I'd like us to try to keep macro performance as good as possible, especially when it's so easy to add the check here.
(Generally I've been heading in the direction of avoiding meta
parsing and writing more specific parse implementations like the one here, for best efficiency.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sweet, i didn't expect this much rigor! i've now gated both parse with is_ident
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw, i think eventually moving everything into pyo3(...)
would also help with perf because then we can skip the non-pyo3 macro parsing completely for attributes that are not ours
Also agreed this is a very interesting idea. Note that inner macro attributes are unstable so it would have to go on each pyclass (or a thing containing pyclasses, maybe a whole module after #2367). Assuming that typically users would set things up to have no pyo3 by default, and then expose a feature which adds a pyo3 dependency, I'm trying to think through the practical usage.
I don't think it's so bad; if a PyO3 dependency is disabled then imports will have a With a separate crate, users can probably just have something like
Or with a stub feature in PyO3, I guess we'd potentially need to make the base PyO3 without features contain absolutely nothing and then enable e.g. So I'm thinking maybe the separate crate is the way to go? It could still depend on |
yeah this would work, but tbh i don't think anybody will run into problems with that, |
On reflection I'm leaning towards merging this (preferably with slight perf tweak as noted above). Although we agree it introduces a weird edge case, you could argue |
yeah this is really more a hack to get this case to work at all. For a real solution we'd need either cooperation from rustc to tell us which ctr_attr epressions would evaluate to true (or just being able to ask it to preprocess the attributes) or some help from cargo to stub out proc macros, as in "don't even compile the proc macro crate". |
BTW this is the reason i wanted to have this feature: https://github.com/konstin/pep440-rs |
I'm reluctant (not necessarily opposed) to merge this. It's a rust edge case; not a pyo3 edge case, so I'd much rather point people to cfg_eval instead. |
@mejrs I know what you mean, initially I felt similar, I've warmed to it a bit when looking at it through the lens of which edge case is least bad for the user. Last I saw I wonder, to give us some control (and way to deprecate this when the language can do better), if we should gate this behavior behind an option? E.g. |
8fc4d91
to
cc35453
Compare
cc35453
to
b1229ab
Compare
Anyone got any further thoughts on this? I can help push this over the line for 0.18, though I'd like some input on what configuration form folks like. Personally, I think having |
Does this mean I as a user would have to use the above syntax every time I invoke |
This assumes people will read the documentation, which I assume they won't. If we do this then few users will discover it. So if we introduce this feature I'd want it to "just work". |
Sorry i'm not following, but which is the solution you'd want? |
I read @mejrs' comment as "don't add either option, and keep this as-is".
I suppose it may be the case that if the language eventually supports a better solution we might be able to transparently just use it without any deprecation. ... in which case, should we just merge this as-is? (After rebase and maybe a squash) |
that either:
I am in the same boat as you: I also conditionally use pyo3. Which is why I wrote #2796 and #2692 but these are done in a way that is unsurprising and composes well. I'm not sure that's true for this PR; it feels rather hacky. I personally would prefer to just tell users to write... impl Foo{
// Rust methods
}
#[cfg(feature = "pyo3")]
#[pymethods]
impl Foo{
// Python methods
} ...which may result in some extra boilerplate, but that's not a disaster or anything. That said, if you are convinced that there are no gotchas or nasty corner cases with it, I'm not opposed to merging this 👍 |
I don't think there are nasty gotchas, though I agree with you it is hacky and the sort of thing we may regret later as a result. I guess worst case if we merge this, we get reports of issues which mean we need to gate it behind the option later on? |
The suggestion to simply recommend a separate pymethods block with a top-level |
b1229ab
to
25fe971
Compare
(sorry this slipped through my notifications, i was just reminded by konstin/pep440-rs#4)
I've already been doing that, I need that for the struct itself, mainly https://github.com/konstin/pep440-rs/blob/3148c9016cbc01a9e6116ae8080b10e14e985487/src/version.rs#L247-L284
i'm rather certain that the solution as-is is the best workaround for the lack of stable |
Hi there, I was wondering whether there was any chance this would merge in the coming weeks. I would use it. My use-case is that I want to allow Python users to set some of the fields of a struct (which are Python-compatible) but not touch the other ones. Thanks |
Sorry, I somehow missed the last reply on this thread from @konstin . So the understanding we have is that The other day I had an idea to add a E.g. the code we have today #[pyclass]
pub struct Foo {
#[pyo3(get, set, name = "bar")]
pub baz: usize,
} could be rewritten using this hypothetical #[pyclass(
getters = (bar = self.baz),
setters = (bar = self.baz)
)]
pub struct Foo {
pub baz: usize,
} which would scale naturally to #[cfg_attr(
feature = "pyo3",
pyclass(
getters = (bar = self.baz),
setters = (bar = self.baz)
)
)]
pub struct Foo {
pub baz: usize,
} Perhaps we should explore that idea further? |
I have no preference ;-) Edit: what is the current workaround for |
Co-authored-by: Georg Brandl <[email protected]>
Co-authored-by: Georg Brandl <[email protected]>
Co-authored-by: Georg Brandl <[email protected]>
25fe971
to
6492f93
Compare
we could definitely move all field attributes to into the container attribute, but this will most likely be more cumbersome both in pyo3 itself and on the user side. I still think the current solution is the best compromise we can get as a workaround for the lack of |
publish to crates.io is also in there but disabled due to PyO3/pyo3#2786
…08 parser Using crates.io releases is currently blocked on PyO3/pyo3#2786
@davidhewitt sorry to bump this up again, but I'm about to embark on a new PyO3 development that will also be behind a feature flag. What is the best approach to solve what this PR solves with version 0.20 (or higher)? Thanks |
I think #2786 (comment) is still most applicable. |
what's the status? |
There does not seem to be progress on |
This adds support for wrapping attributes in
#[cfg_attr(feature = "pyo3", ...)]
, making it possible to build rust libraries with optional python bindings. Checkoptional_bindings.md
for an extensive example.The main trouble with the PR was that some of the attribute parsing is done with
syn::parse::Parse
impls, while other parts are done withsyn::Meta
matching, while syn itself just exposes attribute contents as token stream. Another difficulty is that while#[foo]
is a single attribute, in#[cfg_attr(feature = "something", foo, bar)]
you can have completely unrelated foo and bar attributes.For testing, i had to add a
pyo3
feature to pyo3 itself. I couldn't find a better solution to get apyo3
active in the test, but OTOH that feature shouldn't cause any problems on the user side