-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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 make_simple_namespace function and tests #2840
Conversation
dff2903
to
f80db19
Compare
Indeed, simple namespaces are only available from Python 3.3 onward. That is why some checks are not successful. Please let me know if this MR has any chance of being accepted before I try and think of a fallback for older Python versions. |
ca210c7
to
913f382
Compare
Hi, @joukewitteveen! Thanks for this! A few questions, if you don't mind:
Other than that: shrug, why not, if it can be useful :-) |
tests/test_pytypes.cpp
Outdated
@@ -75,6 +75,19 @@ TEST_SUBMODULE(pytypes, m) { | |||
return dict.contains(val); | |||
}); | |||
|
|||
// test_tuple | |||
m.def("make_tuple", []() { return py::make_tuple(42, py::none(), "spam"); }); |
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.
Isn't this tested anywhere else!? If so, wow, good catch! If not: do we want to repeat this here?
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.
There are other tests that rely on make_tuple
to work, so if it wouldn't work that would not go unnoticed. However, nowhere are specific tests for make_tuple
grouped. I thought it would be good to be a bit more explicit in that regard and, if nothing else, provide a space for such tests to be collected.
913f382
to
9ebef6f
Compare
I needed to pass an instantiated
I added a link to it, but decided against a note on C++ namespaces versus (sub)modules. I'm not sure it is true that those are conceptually linked so strongly. As I understand it, a namespace is pretty fundamental in Python and modules (and classes and everything else) are mostly namespaces with "extra logic". Thus, a
The core API relevant to a |
include/pybind11/cast.h
Outdated
template <typename... Args, | ||
typename = detail::enable_if_t<args_are_all_keyword_or_ds<Args...>()>> | ||
object make_namespace(Args&&... args_) { | ||
return reinterpret_steal<object>(_PyNamespace_New(dict(std::forward<Args>(args_)...).ptr())); |
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.
Can _PyNamespace_New
return NULL
and set an error inside CPython?
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.
It can return NULL, but not sure about setting the error. Error part is on PyDict_New
.
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.
It can indeed return NULL. I'm not sure this will actually happen, but I revised it anyway so that it is more explicit.
Does |
Isn't this the closest you can practically get to C++ namespaces? To me, it seems Python namespaces are basically the lambdas of objects; some kind of anonymous object? Also, we really need to take care of people coming from both C++ as well as Python. The link to the Python docs already helps a bit, though. |
9ebef6f
to
861a644
Compare
I am not sure what you mean here. Can you give a specific code example? Without a
I agree that we should avoid confusion. Do you have any specific addition to the documentation in mind? The way I think of it, a namespace is nothing more than a mapping from names to, in the case of Python, objects. A |
@YannickJadoul @bstaletic Is there anything expected from me to advance this pull request, or is the request simply awaiting its turn in the 2.7 cycle for further decisions? |
Does |
|
I'm saying that if |
861a644
to
c249fc1
Compare
@bstaletic: That makes sense. I tried my hand at making diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h
index e6cfc70..e070026 100644
--- a/include/pybind11/pytypes.h
+++ b/include/pybind11/pytypes.h
@@ -764,7 +764,14 @@ inline bool PyUnicode_Check_Permissive(PyObject *o) { return PyUnicode_Check(o)
#endif
#if PY_MAJOR_VERSION >= 3
-inline bool PyNamespace_Check(PyObject *o) { return isinstance(o, (PyObject *) &_PyNamespace_Type); }
+inline bool PyNamespace_Check(PyObject *o) {
+#if !defined(PYPY_VERSION)
+ auto type = (PyObject *) &_PyNamespace_Type;
+#else
+ auto type = module_::import("types").attr("SimpleNamespace");
+#endif
+ return isinstance(o, type);
+}
#endif
inline bool PyStaticMethod_Check(PyObject *o) { return o->ob_type == &PyStaticMethod_Type; } but I guess |
No.
|
Why? That's what EDIT: Oh, wait. To solve the issue from above, ofc! |
Given this complication with PyPy, I'm wondering, btw: is there any point in doing |
Another possibility is to introduce a
Personally I cannot think of a use case where it makes a lot of sense to check for namespace instances. As I argued before, these simple namespaces are as close to a bare object as I think you can get. I don't think it hurts to think of them as just Shall I revert back to my original |
If it's up to me, and you don't mind, sure. I think I was fine with the PR, but hadn't gotten the chance to click "approve".
Not saying this PR isn't useful or good, but funny you say this: why not just literally create a |
c249fc1
to
e9dc8d7
Compare
Done.
Maybe I am missing something completely, but this would not work because constructing a |
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.
Done.
Thanks a lot! Apologies for the back and forth on this one; guess it's part of figuring out the desired API. But this pretty looks good, to me :-)
If you still feel adventurous, allowing auto d = py::dict("foo"_a = 42, "bar"_a = py::none(); py::make_namespace(**d);
could be fun, but I'm perfectly happy adding that when there's any need for it.
Maybe I am missing something completely, but this would not work because constructing a
py::object
does not instantiate a Python object (it defaults to anullptr
), right? At least.attr
segfaults on a freshly created purepy::object
for me.
Nope, you're not wrong. I was just loosely thinking that maybe there should be a way of constructing an empty py::object
. But let's not dive into that in this PR anymore :-)
No problem. It was a good opportunity to get to know some of the internals of pybind11.
I'm not sure this would be a good idea, but I agree that it looks like a fun addition. I would not encourage people to think of namespaces as dictionaries too much, since namespaces have different key requirement.
Haha, we've come full circle. You can't really have an empty |
Right! >>> x = object()
>>> x.foo = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foo' |
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.
Really nice, thank you @joukewitteveen!
Caveat: I didn't read the many PR comments. But I really like the end result!
docs/advanced/pycpp/object.rst
Outdated
|
||
Attributes on a namespace can be modified with the :func:`py::delattr`, | ||
:func:`py::getattr`, and :func:`py::setattr` functions. Namespaces can be useful | ||
as stand-ins for class instances. |
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.
... as very light-weight stand-ins ...
71e95ae
to
fce90ef
Compare
Thanks, @rwgk, for your suggestions. I implemented them. Since 2.7 is out, I think the corresponding milestone should be closed. It would be cool if that happened automatically when tagging a release. There's probably a way to do that. Note that #2451 introduces functionality that should also be mentioned in the documentation added in this PR. I think it is best to first merge this one and only merge #2451 after it is rebased and amended with some documentation updates. |
My only other qualm is make_namespace to generic / not specific of a name? Especially with C++ namespaces. |
That's a good point. Systematically inserting "simple" would be great. |
I don't think there is a real risk of confusion. At the end of the day, both C++ and Python have namespaces and the objects returned from That being said, I too think adding |
The 2nd version, with the underscore, looks much better to me. |
fce90ef
to
536fa59
Compare
536fa59
to
d4f19c1
Compare
d4f19c1
to
02e38e9
Compare
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.
Awesome, thanks!
Just waiting for the CI to finish, and @Skylion007 to take another look.
Thanks everybody! Merging :-) |
template <typename... Args, | ||
typename = detail::enable_if_t<args_are_all_keyword_or_ds<Args...>()>> | ||
object make_simple_namespace(Args&&... args_) { | ||
PyObject *ns = _PyNamespace_New(dict(std::forward<Args>(args_)...).ptr()); |
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.
Sorry for not noticing this! (I never reviewed this PR, it seems). Why are we using private API here? This private API moved recently in CPython 3.11 development, so this breaks CPython 3.11 support (after alpha 1, 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.
They were going to add this to the public API 2 days ago, but dropped it because:
When I created this PR, I understood that types.SimpleNamespace was immutable. Since it's possible to add new attributes and modify attributes, I'm no longer convinced that it's so useful. It is still possible to access it in C by getting
types.SimpleNamespace
type and then call the type.I prefer to close this PR and move the API to the internal C API.
python/cpython#28970 (comment)
So I think that's the workaround for us.
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.
What was the benefit here over py::module_::import("types").attr("SimpleNamespace")(...)
? Besides being less discoverable with a custom name, it also adds extra header complexity and compile time to expose a private implementation detail of CPython. People should be able to combine simple things to produce complex things, not have a long list of tiny custom functions that have to be learned.
diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp
index 2157dc09..9a1e9188 100644
--- a/tests/test_pytypes.cpp
+++ b/tests/test_pytypes.cpp
@@ -84,7 +84,7 @@ TEST_SUBMODULE(pytypes, m) {
#if PY_VERSION_HEX >= 0x03030000
// test_simple_namespace
m.def("get_simple_namespace", []() {
- auto ns = py::make_simple_namespace("attr"_a=42, "x"_a="foo", "wrong"_a=1);
+ auto ns = py::module_::import("types").attr("SimpleNamespace")("attr"_a=42, "x"_a="foo", "wrong"_a=1);
py::delattr(ns, "wrong");
py::setattr(ns, "right", py::int_(2));
return ns;
This literally passes the test suite. This should not have been added.
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.
My suggestion would be to change the #if
to exclude Python 3.11 and higher, deprecate the feature, and document what you have here as a replacement for the deprecated function.
I could create such a PR tomorrow.
To be honest, I overlooked the _
. Clearly, this always was in the "do we want this" gray area, even assuming there is a public C API, but if that's not true anymore, I agree it's definitely best to remove py::make_simple_namespace()
.
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.
See #3374
Description
This feature request has not been discussed (by me, at least) elsewhere.
Namespaces can be convenient as arguments to Python functions called from C++. In simple cases, they are easier to construct than that classes are instantiated.
On the Python side, these simple namespaces are very simple. They are basically objects on which attributes can be set and removed.
Next to the addition of
make_simple_namespace
, I did some minor expansion of documentation and tests.Suggested changelog entry:
Add ``make_simple_namespace`` function for instantiating Python SimpleNamespace objects.
edit: modified changelog suggestion to reflect the move away from
make_namespace
.edit2: restored changelog suggestion to reflect the move back to
make_namespace
.edit3: modified changelog suggestion to reflect the rename to
make_simple_namespace
.