Skip to content
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

Trouble using PyContextProtocol #1205

Closed
jothan opened this issue Sep 22, 2020 · 12 comments
Closed

Trouble using PyContextProtocol #1205

jothan opened this issue Sep 22, 2020 · 12 comments

Comments

@jothan
Copy link

jothan commented Sep 22, 2020

I'm trying to make a file writing interface be its own context manager with PyO3 0.12. The trouble is that I don't know how to return self from the __enter__ function. I don't have this issue for implementing iterators that are their own iterators.

I can work around this by not using the context protocol and simply using methods inside the pymethods block.

Code that I'm trying to make work: https://github.com/jothan/cordoba/blob/cf8c6deb809d73948c8c47677c6b92b799588131/src/pymod.rs#L200

Iterator that can return self: https://github.com/jothan/cordoba/blob/cf8c6deb809d73948c8c47677c6b92b799588131/src/pymod.rs#L118

@davidhewitt
Copy link
Member

Thanks, yes this is sadly a flaw with the #[pyproto] design at the moment which I've been aware of for a while but not found the right moment to fix. Perhaps this is now a good time. I've opened #1206 and will try to take a spin at this soon.

@kngwyu
Copy link
Member

kngwyu commented Sep 23, 2020

For now, I'm happy to write a temporary solution (i.e. allow PyRefMut) for this.
@davidhewitt What do you think?

@davidhewitt
Copy link
Member

I think what I have in mind is non-breaking and not too hard so I can do over the weekend and we can potentially even release in a 0.12.2.

indygreg added a commit to indygreg/python-zstandard that referenced this issue Dec 31, 2020
I started to implemented this type then quickly found myself blocked
on PyO3/pyo3#1205 /
PyO3/pyo3#1206 due to not being able to return
`Self` from `__enter__`. We'll have to wait for a future pyo3 release
before we can finish the Rust port.
indygreg added a commit to indygreg/python-zstandard that referenced this issue Dec 31, 2020
I started to implemented this type then quickly found myself blocked
on PyO3/pyo3#1205 /
PyO3/pyo3#1206 due to not being able to return
`Self` from `__enter__`. We'll have to wait for a future pyo3 release
before we can finish the Rust port.
@indygreg
Copy link
Contributor

Fun fact: PyContextProtocol isn't required to implement __enter__ and __exit__. Since these methods don't occupy any special slots on PyTypeObject (from Python's C API), you can implement __enter__ and __exit__ next to your normal methods on a #[pymethods] impl block.

You can return self by doing something like:

#[pymethods]
impl MyClass {
    fn __enter__<'p>(slf: PyRef<'p, Self>, _py: Python<'p>) -> PyResult<PyRef<'p, Self>> {
        Ok(slf)
    }
}

You can also use PyRefMut if you need a mutable instance.

I am curious why PyContextProtocol exists in the first place. I naively assumed the various traits existed to facilitate conversion to the appropriate PyTypeObject slots. But __enter__ and __exit__ are defined on tp_methods and don't require special slot handling. So maybe there's another reason for their existence?

@kngwyu
Copy link
Member

kngwyu commented Feb 13, 2021

@indygreg

So maybe there's another reason for their existence?

No, there's no some complex reason. It's just a convention for users: i.e., we treat __dunder__ methods as protocol, even when they are not actually so in CPython API.

@davidhewitt
Copy link
Member

+1 - I'm not aware of the original design choice for #[pyproto]. I think it's because having the trait documentations feels "idiomatic Rust" and it does help to document and group the slot methods.

However I have to confess I've been wondering what happens if we deprecated #[pyproto] in favour of just #[pymethods] (see #1206). It could simplify things. Certainly I'm tempted to experiment in that direction sometime.

@davidhewitt
Copy link
Member

As PyContextProtocol is now effectively deprecated, I'm going to close this.

@samuelcolvin
Copy link
Contributor

Is this still the correct way to call __enter__ today?

    pub fn __enter__(slf: PyRef<Self>) -> PyRef<Self> {
        slf
    }

?

I couldn't find any examples in the docs.

@davidhewitt
Copy link
Member

Yes, that looks right.

As a tiny optimization, if you don't need to access the class contents, I think you can avoid needing to do the runtime borrow check by using Py<Self>:

    pub fn __enter__(slf: Py<Self>) -> Py<Self> {
        slf
    }

(If that doesn't work, it should be trivial for us to make it work.)

@samuelcolvin
Copy link
Contributor

samuelcolvin commented Jun 23, 2022

Thanks so much, seems to work fine, samuelcolvin/watchfiles#164 updated.

@Narsil
Copy link

Narsil commented Jan 19, 2023

Hey, I've been bitten by the fact that __enter__ and __exit__ don't actually drop the underlying Rust struct.

huggingface/safetensors#164

I'm guessing there's not good solution for it, is there ? (since the context_manager variable seems to outlive the context in Python, so maybe PyO3 cannot actually drop).

I could have made the struct be a wrapper itself, containing Option<Inner> and dropping Inner in __exit__ but that felt quite clunky.

Anything I missed ?

@davidhewitt
Copy link
Member

Option<Inner> sounds correct to me; looking at the Python implementation you merged to solve this by assigning a field self.f and have del self.f in the __exit__. The Rust implementation with Option is equivalent, you'd assign self.f = Some(...) in __enter__ and then self.f = None on __exit__.

As you say, the context manager itself can't magically drop itself on __exit__ because the Python reference could live indefinitely long.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants