diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 89b2ac6..65ef3f9 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -10,20 +10,20 @@ are always available. They are listed here in alphabetical order. =================== ================= ================== ================ ==================== .. .. Built-in Functions .. .. =================== ================= ================== ================ ==================== -:func:`abs` |func-dict|_ :func:`help` :func:`min` :func:`setattr` -:func:`all` :func:`dir` :func:`hex` :func:`next` :func:`slice` -:func:`any` :func:`divmod` :func:`id` :func:`object` :func:`sorted` -:func:`ascii` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod` -:func:`bin` :func:`eval` :func:`int` :func:`open` |func-str|_ -:func:`bool` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum` -:func:`bytearray` :func:`filter` :func:`issubclass` :func:`pow` :func:`super` -:func:`bytes` :func:`float` :func:`iter` :func:`print` |func-tuple|_ -:func:`callable` :func:`format` :func:`len` :func:`property` :func:`type` -:func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars` -:func:`classmethod` :func:`getattr` :func:`locals` :func:`repr` :func:`zip` -:func:`compile` :func:`globals` :func:`map` :func:`reversed` :func:`__import__` -:func:`complex` :func:`hasattr` :func:`max` :func:`round` -:func:`delattr` :func:`hash` |func-memoryview|_ |func-set|_ +:func:`abs` |func-dict|_ :func:`help` :func:`min` |func-set|_ +:func:`all` :func:`dir` :func:`hex` :func:`next` :func:`setattr` +:func:`any` :func:`divmod` :func:`id` :func:`object` :func:`slice` +:func:`ascii` :func:`enumerate` :func:`input` :func:`oct` :func:`sorted` +:func:`bin` :func:`eval` :func:`int` :func:`open` :func:`staticmethod` +:func:`bool` :func:`exec` :func:`isinstance` :func:`ord` |func-str|_ +:func:`bytearray` :func:`filter` :func:`issubclass` :func:`pow` :func:`sum` +:func:`bytes` :func:`float` :func:`iter` :func:`print` :func:`super` +:func:`callable` :func:`format` :func:`len` :func:`property` |func-tuple|_ +:func:`chr` |func-frozenset|_ |func-list|_ :func:`public` :func:`type` +:func:`classmethod` :func:`getattr` :func:`locals` |func-range|_ :func:`vars` +:func:`compile` :func:`globals` :func:`map` :func:`repr` :func:`zip` +:func:`complex` :func:`hasattr` :func:`max` :func:`reversed` :func:`__import__` +:func:`delattr` :func:`hash` |func-memoryview|_ :func:`round` =================== ================= ================== ================ ==================== .. using :func:`dict` would create a link to another page, so local targets are @@ -1203,6 +1203,25 @@ are always available. They are listed here in alphabetical order. The docstrings of property objects are now writeable. +.. function:: public(named) + public(**kws) + + May be used as a decorator (e.g. ``@public``) or called as a function with + keyword arguments. When used as a decorator, *named* is an object that has + an ``__name__`` attribute. The value of ``__name__`` is appended to the + module's ``__all__``. In this use case, it is an error to also include + keyword arguments. + + When called as a function, it is an error to pass a positional argument; + only keyword arguments are accepted. The keys must be strings, and they + are inserted into the module's ``__all__``. The mapping of keys to values + is also inserted into the globals, thus creating name bindings in the + module for each key/value pair. + + The module global ``__all__`` is created if it doesn't yet exist. + + .. versionadded:: 3.6 + .. _func-range: .. function:: range(stop) range(start, stop[, step]) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 8cc1b00..d8c6876 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -14,8 +14,11 @@ import traceback import types import unittest import warnings +from contextlib import ExitStack +from importlib import import_module from operator import neg -from test.support import TESTFN, unlink, run_unittest, check_warnings +from test.support import ( + CleanImport, DirsOnSysPath, TESTFN, check_warnings, temp_dir, unlink) from test.support.script_helper import assert_python_ok try: import pty, signal @@ -1823,6 +1826,147 @@ class TestType(unittest.TestCase): type('A', (B,), {'__slots__': '__weakref__'}) +class TestPublic(unittest.TestCase): + import_line = 'from public import public' + + def setUp(self): + self.resources = ExitStack() + self.tmpdir = self.resources.enter_context(temp_dir()) + self.resources.enter_context(DirsOnSysPath(self.tmpdir)) + self.resources.enter_context(CleanImport('public')) + self.addCleanup(self.resources.close) + + def test_atpublic_function(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +@public +def a_function(): + pass +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.__all__, ['a_function']) + + def test_atpublic_function_runnable(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +@public +def a_function(): + return 1 +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.a_function(), 1) + + def test_atpublic_class(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +@public +class AClass: + pass +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.__all__, ['AClass']) + + def test_atpublic_class_runnable(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +@public +class AClass: + pass +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertIsInstance(module.AClass(), module.AClass) + + def test_atpublic_two_things(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +@public +def foo(): + pass + +@public +class AClass: + pass +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.__all__, ['foo', 'AClass']) + + def test_atpublic_append_to_all(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +__all__ = ['a', 'b'] + +a = 1 +b = 2 + +@public +def foo(): + pass + +@public +class AClass: + pass +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.__all__, ['a', 'b', 'foo', 'AClass']) + + def test_atpublic_keywords(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +public(a=1, b=2) +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(sorted(module.__all__), ['a', 'b']) + + def test_atpublic_keywords_multicall(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +public(b=1) +public(a=2) +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.__all__, ['b', 'a']) + + def test_atpublic_keywords_global_bindings(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +public(a=1, b=2) +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.a, 1) + self.assertEqual(module.b, 2) + + def test_atpublic_mixnmatch(self): + modpath = os.path.join(self.tmpdir, 'public.py') + with open(modpath, 'w', encoding='utf-8') as fp: + print("""\ +__all__ = ['a', 'b'] + +a = 1 +b = 2 + +@public +def foo(): + pass + +@public +class AClass: + pass + +public(c=3) +""".format(self.import_line), file=fp) + module = import_module('public') + self.assertEqual(module.__all__, ['a', 'b', 'foo', 'AClass', 'c']) + + def load_tests(loader, tests, pattern): from doctest import DocTestSuite tests.addTest(DocTestSuite(builtins)) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 0637a2d..cc24390 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1311,6 +1311,115 @@ Return the next item from the iterator. If default is given and the iterator\n\ is exhausted, it is returned instead of raising StopIteration."); +/* AC: cannot convert yet, as needs arbitrary keyword arguments. */ +static PyObject * +builtin_public(PyObject *self, PyObject *args, PyObject *kwds) +{ + PyObject *arg = NULL; + PyObject *globals = NULL; + PyObject *all = NULL; + PyObject *rtn = NULL; + + if (!PyArg_UnpackTuple(args, "public", 0, 1, &arg)) + return NULL; + + /* kwds can be empty, but the keys must be strings. */ + if (kwds != NULL && !PyArg_ValidateKeywordArguments(kwds)) + return NULL; + + if (!(globals = PyEval_GetGlobals())) { + PyErr_Format(PyExc_TypeError, + "@public called with no active globals"); + return NULL; + } + if (!(all = PyDict_GetItemString(globals, "__all__"))) { + if (!(all = PyList_New(0))) + return NULL; + if (PyDict_SetItemString(globals, "__all__", all) < 0) + goto byebye; + } + else + /* Bump all's reference count since it's currently borrowed, and this + way we guarantee we own a reference for common exit cleanup. + */ + Py_INCREF(all); + + if (arg != NULL) { + PyObject *name = NULL; + + /* There is a single positional argument. This must have a "__name__" + attribute, which we will put in __all__. The keywords dictionary + must be empty. + */ + if (kwds != NULL && PyDict_Size(kwds) != 0) { + PyErr_Format(PyExc_TypeError, + "Positional and keywords are mutually exclusive"); + goto byebye; + } + if (!PyObject_HasAttrString(arg, "__name__")) { + PyErr_Format(PyExc_TypeError, + "Positional argument has no __name__"); + goto byebye; + } + if (!(name = PyObject_GetAttrString(arg, "__name__")) || + !PyUnicode_Check(name)) + { + PyErr_Format(PyExc_TypeError, "Bad __name__ value"); + Py_XDECREF(name); + goto byebye; + } + if (PyList_Append(all, name) < 0) { + Py_DECREF(name); + goto byebye; + } + Py_DECREF(name); + rtn = arg; + } + else if (kwds == NULL || PyDict_Size(kwds) == 0) { + PyErr_Format( + PyExc_TypeError, + "Either a single positional or keyword arguments required"); + goto byebye; + } + else { + /* There are only keyword arguments, so for each of these, insert the + key in __all__ *and* bind the name/key to the value in globals. We + force the use of globals here because we're modifying the global + __all__ so it doesn't make sense to touch locals. + */ + PyObject *key, *value; + Py_ssize_t pos = 0; + + while (PyDict_Next(kwds, &pos, &key, &value)) { + if (PyList_Append(all, key) < 0) + goto byebye; + if (PyDict_SetItem(globals, key, value) < 0) + goto byebye; + } + rtn = Py_None; + } + + byebye: + Py_DECREF(all); + Py_XINCREF(rtn); + return rtn; +} + +PyDoc_STRVAR(public_doc, +"public(named)\n\ +public(**kws)\n\ +\n\ +May be used as a decorator or called directly. When used as a decorator\n\ +the thing being decorated must have an __name__ attribute. The value of\n\ +__name__ will be inserted in the global __all__ list. In this use case\n\ +it is an error to also include keyword arguments.\n\ +\n\ +When called directly, it is an error to pass a positional argument. The\n\ +keys must be strings, and are inserted into the global __all__ list. The\n\ +mapping of keys to values is also inserted into the globals, thus creating\n\ +name bindings for each key/value pair."); + + /*[clinic input] setattr as builtin_setattr @@ -2619,6 +2728,7 @@ static PyMethodDef builtin_methods[] = { BUILTIN_ORD_METHODDEF BUILTIN_POW_METHODDEF {"print", (PyCFunction)builtin_print, METH_VARARGS | METH_KEYWORDS, print_doc}, + {"public", (PyCFunction)builtin_public, METH_VARARGS | METH_KEYWORDS, public_doc}, BUILTIN_REPR_METHODDEF {"round", (PyCFunction)builtin_round, METH_VARARGS | METH_KEYWORDS, round_doc}, BUILTIN_SETATTR_METHODDEF