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

Add make_simple_namespace function and tests #2840

Merged
merged 1 commit into from
Aug 26, 2021

Conversation

joukewitteveen
Copy link
Contributor

@joukewitteveen joukewitteveen commented Feb 1, 2021

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.

@joukewitteveen
Copy link
Contributor Author

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.

@joukewitteveen joukewitteveen force-pushed the feature-make_namespace branch 2 times, most recently from ca210c7 to 913f382 Compare February 1, 2021 15:49
@YannickJadoul
Copy link
Collaborator

Hi, @joukewitteveen! Thanks for this!

A few questions, if you don't mind:

  • I personally never had any use for these, but I'm assuming you have something in mind?
  • Is there any Python documentation on this? I'm wondering if we should link to it in the docs, to avoid confusion with C++ namespaces. At any rate, maybe you could still add a note that the equivalent of C++ namespaces would normally just be def_submodule?
  • Is there a point in having a py::namespace object? Since you're making the comparison to py::tuple/py::make_tuple, that would make sense. But at the same time, there doesn't seem to be a lot of API related to Python's namespaces (and hardly any docs, it seems).

Other than that: shrug, why not, if it can be useful :-)

@@ -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"); });
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

@YannickJadoul YannickJadoul added this to the v2.7 milestone Feb 1, 2021
@joukewitteveen
Copy link
Contributor Author

* I personally never had any use for these, but I'm assuming you have something in mind?

I needed to pass an instantiated Logger class in a Python call. I wanted logging to be taken care of by C++ functions, and using a namespace with a few attributes (cppfunctions) is easier than instantiating and configuring a class.

* Is there any Python documentation on this? I'm wondering if we should link to it in the docs, to avoid confusion with C++ namespaces. At any rate, maybe you could still add a note that the equivalent of C++ namespaces would normally just be `def_submodule`?

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 SimpleNamespace (from make_namespace) can in fact be thought of as an (maybe not "the") equivalent of a C++ namespace.

* Is there a point in having a `py::namespace` object? Since you're making the comparison to `py::tuple`/`py::make_tuple`, that would make sense. But at the same time, there doesn't seem to be a lot of API related to Python's namespaces (and hardly any docs, it seems).

The core API relevant to a SimpleNamespace, I think, consists of hasattr, delattr, getattr, setattr. Those are present on objects, so we don't need a py::namespace. In fact, make_namespace() is one way to get an "empty" object (not just what effectively amounts to a nullptr).

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()));
Copy link
Collaborator

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?

Copy link
Collaborator

Choose a reason for hiding this comment

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

https://github.com/python/cpython/blob/63298930fb531ba2bb4f23bc3b915dbf1e17e9e1/Objects/namespaceobject.c#L33

It can return NULL, but not sure about setting the error. Error part is on PyDict_New.

Copy link
Contributor Author

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.

@bstaletic
Copy link
Collaborator

Does py::isinstance work with these things? Usually there's PyThing_Check(PyThingObject*), but there isn't a PyNamespace_Check. However, besides the _PyNamespace_New(), we also have access to _PyNamespace_Type type object. So... if PyObject_IsInstance(namespace_ptr, &_PyNamespace_Type); works, py::isinstance should work as well. At that point, it would be inconsistent to allow py::isinstance(object, type) to work, but not py::isinstance<pybind_type>(object).

@YannickJadoul
Copy link
Collaborator

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.

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?
C++ namespaces and Python submodules are +- meant to organize functions, classes, ... hierarchically within a project, so are to me (practially speaking) much closer.

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.

@joukewitteveen
Copy link
Contributor Author

Does py::isinstance work with these things? Usually there's PyThing_Check(PyThingObject*), but there isn't a PyNamespace_Check. However, besides the _PyNamespace_New(), we also have access to _PyNamespace_Type type object. So... if PyObject_IsInstance(namespace_ptr, &_PyNamespace_Type); works, py::isinstance should work as well. At that point, it would be inconsistent to allow py::isinstance(object, type) to work, but not py::isinstance<pybind_type>(object).

I am not sure what you mean here. Can you give a specific code example? Without a py::namespace type, it is unclear to me how this is relevant.

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.

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?
C++ namespaces and Python submodules are +- meant to organize functions, classes, ... hierarchically within a project, so are to me (practially speaking) much closer.

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.

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 SimpleNamespace does this in a very bare bones manner. I'm not sure what you mean by 'anonymous object', but indeed, modules usually have more attributes. Whether this makes them more akin to C++ namespaces I don't know. Both can provide a hierarchical organization of functions, classes, etc..

@joukewitteveen
Copy link
Contributor Author

@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?

@bstaletic
Copy link
Collaborator

I am not sure what you mean here. Can you give a specific code example? Without a py::namespace type, it is unclear to me how this is relevant.

Does py::isinstance(namespace_object, &_PyNamespace_Type); return true?

@joukewitteveen
Copy link
Contributor Author

Does py::isinstance(namespace_object, &_PyNamespace_Type); return true?

py::isinstance(obj, (PyObject *) &_PyNamespace_Type); returns true on namespace object and false on others (e.g. tuples). Shall I add this observation to the tests?

@bstaletic
Copy link
Collaborator

I'm saying that if py::isinstance works, and you've just said that it does, this warrants a proper py::namespace type. Why should _PyNamespace_Type be different from all the other python types that pybind11 supports?

@joukewitteveen joukewitteveen changed the title Add make_namespace function and tests Add namespace_ type and tests Feb 15, 2021
@joukewitteveen
Copy link
Contributor Author

@bstaletic: That makes sense. I tried my hand at making namespace_ a proper type (do we want using namespace = namespace_ to enable py::namespace?). It fails on PyPy, where SimpleNamespaces are implemented as Python classes. I would like to do

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 module_ is not available in pytypes.h. Any suggestions?

@bstaletic
Copy link
Collaborator

Do we want using namespace = namespace_ to enable py::namespace?

No. using module = module_; is there for backwards compatibility. You'll notice that there isn't py::class, only py::class_.

module_::import("types").attr("SimpleNamespace");

auto types_module = py::reinterpret_steal<py::object>(PyImport_ImportModule("types")); 

@YannickJadoul
Copy link
Collaborator

YannickJadoul commented Feb 15, 2021

module_::import("types").attr("SimpleNamespace");

auto types_module = py::reinterpret_steal<py::object>(PyImport_ImportModule("types")); 

Why? That's what module_::import("types") is also doing, no?

EDIT: Oh, wait. To solve the issue from above, ofc!

@YannickJadoul
Copy link
Collaborator

Given this complication with PyPy, I'm wondering, btw: is there any point in doing isinstance(obj, py::namespace_)? These objects are completely meant as duck-typed things, anyway, right? Is there then any point in making this a full py::object subtype?

@joukewitteveen
Copy link
Contributor Author

Another possibility is to introduce a detail::namespace_type, which is set to (PyObject *) &_PyNamespace_Type with Python and to module_::import("types").attr("SimpleNamespace") with PyPy. The latter definition would be placed below the definition of class module_ in pybind11.h.
This does mean we import types whenever we run with PyPy.

Given this complication with PyPy, I'm wondering, btw: is there any point in doing isinstance(obj, py::namespace_)? These objects are completely meant as duck-typed things, anyway, right? Is there then any point in making this a full py::object subtype?

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 py::objects.

Shall I revert back to my original make_namespace proposal?

@YannickJadoul
Copy link
Collaborator

Shall I revert back to my original make_namespace proposal?

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".

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 py::objects.

Not saying this PR isn't useful or good, but funny you say this: why not just literally create a py::object and assign with obj.attr("foo") = ...? Just out of curiosity; not saying this PR needs a big overhaul!
I recently also had a situation where this would have been useful, btw :-)

@joukewitteveen joukewitteveen force-pushed the feature-make_namespace branch from c249fc1 to e9dc8d7 Compare March 1, 2021 08:40
@joukewitteveen
Copy link
Contributor Author

Shall I revert back to my original make_namespace proposal?

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".

Done.

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 py::objects.

Not saying this PR isn't useful or good, but funny you say this: why not just literally create a py::object and assign with obj.attr("foo") = ...? Just out of curiosity; not saying this PR needs a big overhaul!
I recently also had a situation where this would have been useful, btw :-)

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 a nullptr), right? At least .attr segfaults on a freshly created pure py::object for me.

@joukewitteveen joukewitteveen changed the title Add namespace_ type and tests Add make_namespace function and tests Mar 1, 2021
Copy link
Collaborator

@YannickJadoul YannickJadoul left a 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 a nullptr), right? At least .attr segfaults on a freshly created pure py::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 :-)

@joukewitteveen
Copy link
Contributor Author

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 :-)

No problem. It was a good opportunity to get to know some of the internals of pybind11.

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.

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.

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 a nullptr), right? At least .attr segfaults on a freshly created pure py::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 :-)

Haha, we've come full circle. You can't really have an empty py::object, because there is no such thing as a bare object type. One thing that comes real close to it will be py::make_namespace() ;-).

@YannickJadoul
Copy link
Collaborator

YannickJadoul commented Mar 1, 2021

Haha, we've come full circle. You can't really have an empty py::object, because there is no such thing as a bare object type. One thing that comes real close to it will be py::make_namespace() ;-).

Right!
Well, also kind of wrong: you can make an object() (I've seen it being used as sentinel = object() when None is a valid value; see here). But you can't assign attributes to it, so I did miss that, indeed!

>>> x = object()
>>> x.foo = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foo'

Copy link
Collaborator

@rwgk rwgk left a 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!


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.
Copy link
Collaborator

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 ...

include/pybind11/cast.h Outdated Show resolved Hide resolved
docs/advanced/pycpp/object.rst Outdated Show resolved Hide resolved
tests/test_pytypes.cpp Outdated Show resolved Hide resolved
@joukewitteveen joukewitteveen force-pushed the feature-make_namespace branch from 71e95ae to fce90ef Compare August 22, 2021 16:45
@joukewitteveen
Copy link
Contributor Author

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.

@Skylion007
Copy link
Collaborator

My only other qualm is make_namespace to generic / not specific of a name? Especially with C++ namespaces.

@rwgk
Copy link
Collaborator

rwgk commented Aug 24, 2021

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.

@joukewitteveen
Copy link
Contributor Author

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 make_namespace fit the Python definition of a namespace.

That being said, I too think adding simple is a good idea (although the name gets a bit long). Shall I go with make_simplenamespace or make_simple_namespace?

@rwgk
Copy link
Collaborator

rwgk commented Aug 24, 2021

That being said, I too think adding simple is a good idea (although the name gets a bit long). Shall I go with make_simplenamespace or make_simple_namespace?

The 2nd version, with the underscore, looks much better to me.
(I think code will be easier to read with the longer name. Without the "simple" people would first go huh? until they go look up what it is.)

@joukewitteveen joukewitteveen force-pushed the feature-make_namespace branch from fce90ef to 536fa59 Compare August 24, 2021 15:25
@joukewitteveen joukewitteveen requested a review from rwgk August 24, 2021 15:27
tests/test_pytypes.cpp Outdated Show resolved Hide resolved
@joukewitteveen joukewitteveen force-pushed the feature-make_namespace branch from 536fa59 to d4f19c1 Compare August 24, 2021 15:48
@joukewitteveen joukewitteveen force-pushed the feature-make_namespace branch from d4f19c1 to 02e38e9 Compare August 24, 2021 15:49
Copy link
Collaborator

@rwgk rwgk left a 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.

@rwgk rwgk changed the title Add make_namespace function and tests Add make_simple_namespace function and tests Aug 24, 2021
@rwgk
Copy link
Collaborator

rwgk commented Aug 26, 2021

Thanks everybody! Merging :-)

@rwgk rwgk merged commit 031a700 into pybind:master Aug 26, 2021
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label Aug 26, 2021
@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label Sep 16, 2021
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());
Copy link
Collaborator

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).

Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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().

Copy link
Collaborator

Choose a reason for hiding this comment

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

See #3374

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

Successfully merging this pull request may close these issues.

6 participants