././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1618366884.052773 atpublic-2.3/0000775000076500000240000000000000000000000012166 5ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603401986.0 atpublic-2.3/.readthedocs-req.txt0000664000076500000240000000003100000000000016051 0ustar00barrystaffsphinx_autodoc_typehints ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609564110.0 atpublic-2.3/LICENSE0000664000076500000240000000105400000000000013173 0ustar00barrystaffCopyright 2016-2021 Barry Warsaw Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1615265570.0 atpublic-2.3/MANIFEST.in0000664000076500000240000000035100000000000013723 0ustar00barrystaffinclude *.py MANIFEST.in LICENSE README.rst global-include *.txt *.rst *.ini *.c *.h *.cfg py.typed recursive-include test *.py recursive-include docs *.py exclude .gitignore exclude *.so prune build prune dist prune .tox prune .git ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1618366884.0530467 atpublic-2.3/PKG-INFO0000664000076500000240000000376300000000000013274 0ustar00barrystaffMetadata-Version: 2.1 Name: atpublic Version: 2.3 Summary: public -- @public for populating __all__ Home-page: http://public.readthedocs.io/ Author: Barry Warsaw Author-email: barry@python.org License: Apache 2.0 Project-URL: Documentation, https://public.readthedocs.io Project-URL: Source, https://gitlab.com/warsaw/public.git Project-URL: Tracker, https://gitlab.com/warsaw/public/issues Description: ====================== @public and @private ====================== This library provides two very simple decorators that document the *publicness* of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Author ====== ``public`` is Copyright (C) 2016-2021 Barry Warsaw Licensed under the terms of the Apache License Version 2.0. See the LICENSE file for details. Project details =============== * Project home: https://gitlab.com/warsaw/public * Report bugs at: https://gitlab.com/warsaw/public/issues * Code hosting: https://gitlab.com/warsaw/public.git * Documentation: https://public.readthedocs.io * PyPI: https://pypi.python.org/pypi/atpublic Keywords: __all__ public Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: >=3.6 Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609564110.0 atpublic-2.3/README.rst0000664000076500000240000000132100000000000013652 0ustar00barrystaff====================== @public and @private ====================== This library provides two very simple decorators that document the *publicness* of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Author ====== ``public`` is Copyright (C) 2016-2021 Barry Warsaw Licensed under the terms of the Apache License Version 2.0. See the LICENSE file for details. Project details =============== * Project home: https://gitlab.com/warsaw/public * Report bugs at: https://gitlab.com/warsaw/public/issues * Code hosting: https://gitlab.com/warsaw/public.git * Documentation: https://public.readthedocs.io * PyPI: https://pypi.python.org/pypi/atpublic ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1618366883.9118412 atpublic-2.3/atpublic.egg-info/0000775000076500000240000000000000000000000015463 5ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618366883.0 atpublic-2.3/atpublic.egg-info/PKG-INFO0000664000076500000240000000376300000000000016571 0ustar00barrystaffMetadata-Version: 2.1 Name: atpublic Version: 2.3 Summary: public -- @public for populating __all__ Home-page: http://public.readthedocs.io/ Author: Barry Warsaw Author-email: barry@python.org License: Apache 2.0 Project-URL: Documentation, https://public.readthedocs.io Project-URL: Source, https://gitlab.com/warsaw/public.git Project-URL: Tracker, https://gitlab.com/warsaw/public/issues Description: ====================== @public and @private ====================== This library provides two very simple decorators that document the *publicness* of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Author ====== ``public`` is Copyright (C) 2016-2021 Barry Warsaw Licensed under the terms of the Apache License Version 2.0. See the LICENSE file for details. Project details =============== * Project home: https://gitlab.com/warsaw/public * Report bugs at: https://gitlab.com/warsaw/public/issues * Code hosting: https://gitlab.com/warsaw/public.git * Documentation: https://public.readthedocs.io * PyPI: https://pypi.python.org/pypi/atpublic Keywords: __all__ public Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: >=3.6 Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618366883.0 atpublic-2.3/atpublic.egg-info/SOURCES.txt0000664000076500000240000000104500000000000017347 0ustar00barrystaff.readthedocs-req.txt LICENSE MANIFEST.in README.rst conftest.py setup.cfg setup.py setup_helpers.py tox.ini atpublic.egg-info/PKG-INFO atpublic.egg-info/SOURCES.txt atpublic.egg-info/dependency_links.txt atpublic.egg-info/not-zip-safe atpublic.egg-info/requires.txt atpublic.egg-info/top_level.txt docs/NEWS.rst docs/__init__.py docs/apiref.rst docs/conf.py docs/index.rst docs/using.rst public/__init__.py public/private.py public/public.py public/py.typed public/types.py test/__init__.py test/test_mypy.py test/test_private.py test/test_public.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618366883.0 atpublic-2.3/atpublic.egg-info/dependency_links.txt0000664000076500000240000000000100000000000021531 0ustar00barrystaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618366883.0 atpublic-2.3/atpublic.egg-info/not-zip-safe0000664000076500000240000000000100000000000017711 0ustar00barrystaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618366883.0 atpublic-2.3/atpublic.egg-info/requires.txt0000664000076500000240000000005500000000000020063 0ustar00barrystaff [:python_version < "3.8"] typing_extensions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618366883.0 atpublic-2.3/atpublic.egg-info/top_level.txt0000664000076500000240000000000700000000000020212 0ustar00barrystaffpublic ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603401986.0 atpublic-2.3/conftest.py0000664000076500000240000000633400000000000014373 0ustar00barrystaffimport os import sys from contextlib import ExitStack, contextmanager from doctest import ELLIPSIS, REPORT_NDIFF, NORMALIZE_WHITESPACE from sybil import Sybil from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser from tempfile import TemporaryDirectory from types import ModuleType import pytest DOCTEST_FLAGS = ELLIPSIS | NORMALIZE_WHITESPACE | REPORT_NDIFF @contextmanager def syspath(directory): try: sys.path.insert(0, directory) yield finally: assert sys.path[0] == directory del sys.path[0] @contextmanager def sysmodules(): modules = sys.modules.copy() try: yield finally: sys.modules = modules class ExampleModule: def __init__(self, path): self.path = path def __call__(self, contents): with open(self.path, 'w', encoding='utf-8') as fp: fp.write(contents) @pytest.fixture def example(): with ExitStack() as resources: tmpdir = resources.enter_context(TemporaryDirectory()) resources.enter_context(sysmodules()) resources.enter_context(syspath(tmpdir)) path = os.path.join(tmpdir, 'example.py') yield ExampleModule(path) class DoctestNamespace: def setup(self, namespace): # The doctests in .rst files require that they mimic being executed in # a particular module. The stdlib doctest functionality creates its # own globals namespace, unattached to any specific module object. # This causes coordination problems between the apparent globals that # the doctest sees, and public()'s implementation. # # We can't make them the same namespace because doing so violates # other assumptions in the public() function's code, but we can set # things up to be close enough for the doctest to pass. # # We use two techniques to make this work. First, we create a test # module and ensure that its string name is assigned to the # namespace's __name__ attribute. We also ensure that the module by # that name is in the sys.modules cache (and cleaned up in the # teardown). # # The second thing we need to do is to ensure that the module and the # namespace the doctest is executed in, share the same list object in # their __all__ attribute. Now, generally public() will create # __all__ if it doesn't exist, but we can test that in the unittests, # so it's good enough to just initialize both name bindings to the # same list object here. # # There is some further discussion in this Sybil ticket: # https://github.com/cjw296/sybil/issues/21 self._testmod = ModuleType('testmod') namespace['__name__'] = self._testmod.__name__ sys.modules[self._testmod.__name__] = self._testmod self._testmod.__all__ = namespace['__all__'] = [] def teardown(self, namespace): del sys.modules[self._testmod.__name__] namespace = DoctestNamespace() pytest_collect_file = Sybil( parsers=[ DocTestParser(optionflags=DOCTEST_FLAGS), CodeBlockParser(), ], pattern='*.rst', setup=namespace.setup, ).pytest() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1618366883.9220424 atpublic-2.3/docs/0000775000076500000240000000000000000000000013116 5ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618363452.0 atpublic-2.3/docs/NEWS.rst0000664000076500000240000000356100000000000014431 0ustar00barrystaff============== @public NEWS ============== 2.3 (2021-04-13) ================ * Do type hinting the right way. (GL#10) 2.2 (2021-04-13) ================ * ``public()`` and ``private()`` can't be correctly type annotated, so the type hints on these two functions have been removed. The ``ModuleAware`` was also removed. (GL#10) * Added a ``py.typed`` file to satisfy type checkers. (GL#9) * Fixed a documentation cross-reference bug. 2.1.3 (2021-02-15) ================== * I `blue `_ it! 2.1.2 (2021-01-01) ================== * Update copyright years. * Include ``test/__init__.py`` and ``docs/__init__.py`` (GL#9) 2.1.1 (2020-10-22) ================== * Rename top-level tests/ directory to test/ (GL#8) 2.1 (2020-10-21) ================ * Clean up some typing problems. * Reorganized docs and tests out of the code directory (GL#7). * Fix the Windows CI tests. 2.0 (2020-07-27) ================ * Drop Python 3.4 and 3.5; add Python 3.8 and 3.9. * The C implementation is removed. (GL#4) * Added an ``@private`` decorator (GL#3) * Build and test on Windows in addition to Linux. * Fix the doctests so that they actually run and pass! * Add type annotations and API reference documentation. * Internal improvements and modernizations. 1.0 (2017-09-15) ================ * 1.0 release. * Documentation improvements. 0.5 (2016-12-14) ================ * Fix MANIFEST.in inclusion of the src directory for the C extension. 0.4 (2016-11-28) ================ * Add Python 3.6 support. * Make building the C extension optional, for environments without a C compiler. 0.3 (2016-05-25) ================ * Raise ``ValueError`` when ``__all__`` isn't a list (or subclass) instance. 0.2 (2016-05-22) ================ * Documentation updates based on initial feedback. * Some minor test suite clean up. 0.1 (2016-05-09) ================ * Initial release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603401986.0 atpublic-2.3/docs/__init__.py0000664000076500000240000000000000000000000015215 0ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618362497.0 atpublic-2.3/docs/apiref.rst0000664000076500000240000000025700000000000015122 0ustar00barrystaff============= API Reference ============= API reference for ``public``: .. autofunction:: public.public .. autofunction:: public.private .. autofunction:: public.install ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618355324.0 atpublic-2.3/docs/conf.py0000664000076500000240000001565300000000000014427 0ustar00barrystaff# -*- coding: utf-8 -*- # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx_autodoc_typehints', ] intersphinx_mapping = { 'python': ('https://docs.python.org/', None), } # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'public' copyright = u'2016-2021 by Barry Warsaw' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. from public import __version__ as version # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'eggs', '.tox'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'public' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'public.tex', u'public Documentation', u'Barry Warsaw', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'public', u'public Documentation', [u'Barry Warsaw'], 1) ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618295204.0 atpublic-2.3/docs/index.rst0000664000076500000240000000431400000000000014761 0ustar00barrystaff========================================================== @public and @private -- Document your module's interface ========================================================== .. currentmodule:: public This library provies two very simple decorators that document the *publicness* of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Please note that while the package is called :doc:`public ` and it provides a top-level module named ``public``, the PyPI package is called ``atpublic`` due to name conflicts. Requirements ============ ``public`` requires Python 3.6 or newer. Documentation ============= A `simple guide`_ to using the library is available, along with a detailed `API reference`_. Project details =============== * Project home: https://gitlab.com/warsaw/public * Report bugs at: https://gitlab.com/warsaw/public/issues * Code hosting: https://gitlab.com/warsaw/public.git * Documentation: https://public.readthedocs.io * PyPI: https://pypi.python.org/pypi/atpublic You can install it with `pip`:: % pip install atpublic **Do not install "public"; that is a different package!** You can grab the latest development copy of the code using git. The master repository is hosted on GitLab. If you have git installed, you can grab your own branch of the code like this:: $ git clone https://gitlab.com/warsaw/public.git You may contact the author via barry@python.org. Copyright ========= Copyright (C) 2016-2021 Barry A. Warsaw Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Table of Contents and Index =========================== * :ref:`genindex` .. toctree:: :glob: using apiref NEWS .. _`simple guide`: using.html .. _`API reference`: apiref.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603401986.0 atpublic-2.3/docs/using.rst0000664000076500000240000001750700000000000015007 0ustar00barrystaff====================== @public and @private ====================== This library provies two very simple decorators that document the *publicness* of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Background ========== ``__all__`` is great. It has both functional and documentation purposes. The functional purpose is that it `directly controls`_ which module names are imported by the ``from import *`` statement. In the absence of an ``__all__``, when this statement is executed, every name in ```` that does not start with an underscore will be imported. This often leads to importing too many names into the module. That's a good enough reason not to use ``from import *`` with modules that don't have an ``__all__``. In the presence of an ``__all__``, only the names specified in this list are imported by the ``from import *`` statement. This in essence gives the ```` author a way to explicitly state which names are for public consumption. And that's the second purpose of ``__all__``; it serves as module documentation, explicitly naming the public objects it wants to export. You can print a module's ``__all__`` and get an explicit declaration of its public API. The problem =========== ``__all__`` has two problems. First, it separates the declaration of a name's public export semantics from the implementation of that name. Usually the ``__all__`` is put at the top of the module, although this isn't required, and in some cases it's `actively prohibited`_. So when you're looking at the definition of a function or class in a module, you have to search for the ``__all__`` definition to know whether the function or class is intended for public consumption. This leads to the second problem, which is that it's too easy for the ``__all__`` to get `out of sync`_ with the module's contents. Often a function or class is renamed, removed, or added without the ``__all__`` being updated. Then it's difficult to know what the module author's intent was, and it can lead to an exception when a string appearing in ``__all__`` doesn't match an existing name in the module. Some tools like Sphinx_ will complain when names appear in ``__all__`` don't appear in the module. All of this points to the root problem; it should be easy to keep ``__all__`` in sync! The solution ============ This package provides a way to declare a name's *publicness* right at the point of its declaration, and to infer the name to export from that definition. In this way, a module's author never explicitly sets the ``__all__`` so there's no way for it to get out of sync. This package, and Python `issue 26632`_, propose just such a solution, in the form of a ``public`` builtin that can be used as either a decorator, or a callable. >>> from public import public You'll usually use this as a decorator, for example:: >>> @public ... def foo(): ... pass or:: >>> @public ... class Bar: ... pass The ``__all__`` after both of those code snippets has both names in it:: >>> print(__all__) ['foo', 'Bar'] Note that you do not need to initialize ``__all__`` in the module, since ``public`` will do it for you. Of course, if your module *already* has an ``__all__``, it will add any new names to the existing list. The requirements to use the ``@public`` decorator are simple: the decorated thing must have a ``__name__`` attribute. Since you'll overwhelmingly use it to decorate functions and classes, this will always be the case. If the object has a ``__module__`` attribute, that string is used to look up the module object in ``sys.modules``, otherwise the module is extracted from the globals where the decorator is called. There's one other common use case that isn't covered by the ``@public`` decorator. Sometimes you want to declare simple constants or instances as publicly available. You can't use the ``@public`` decorator for two reasons: constants don't have a ``__name__`` and Python's syntax doesn't allow you to decorate such constructs. To solve this use case, ``public`` is also a callable function accepting keyword arguments. An example makes this obvious:: >>> public(SEVEN=7) >>> public(a_bar=Bar()) The module's ``__all__`` now contains both of the keys:: >>> print(__all__) ['foo', 'Bar', 'SEVEN', 'a_bar'] and as should be obvious, the module contains name bindings for these constants:: >>> print(SEVEN) 7 >>> print(a_bar) <....Bar object at ...> Multiple keyword arguments are allowed:: >>> public(ONE=1, TWO=2) >>> print(__all__) ['foo', 'Bar', 'SEVEN', 'a_bar', 'ONE', 'TWO'] >>> print(ONE) 1 >>> print(TWO) 2 @private ======== You might also want to be explicit about your private, i.e. non-public names. This library also provides an ``@private`` decorator for this purpose. While it mostly serves for documentation purposes, this decorator also ensures that the decorated object's name does *not* appear in the ``__all__``:: >>> from public import private >>> @private ... def foo(): ... pass >>> print(__all__) ['Bar', 'SEVEN', 'a_bar', 'ONE', 'TWO'] You can see here that ``foo`` has been removed from the ``__all__``. It's okay if the name doesn't appear in ``__all__`` at all:: >>> @private ... class Baz: ... pass >>> print(__all__) ['Bar', 'SEVEN', 'a_bar', 'ONE', 'TWO'] In this case, ``Baz`` never appears in ``__all__``. Like with ``@public``, the ``@private`` decorator will add any missing ``__all__``, but if it exists in the module, it must be a list. There is no functional API for ``@private``. Making @public and @private built-ins ===================================== It can get rather tedious if you have to add the above import in every module where you want to use it. What if you could put ``public`` into Python's builtins_? Then it would be available in all your code for free:: >>> from public import install >>> install() and now you can just use ``@public`` and ``@private`` without having to import anything in your other modules. Caveats ======= There are some important usage restrictions you should be aware of: * Only use ``@public`` and ``@private`` on top-level object. Specifically, don't try to use either decorator on a class method name. While the declaration won't fail, you will get an exception when you attempt to ``from import *`` because the name pulled from ``__all__`` won't be in the module's globals. * If you explicitly set ``__all__`` in your module, be sure to set it to a list. Some style guides require ``__all__`` to be a tuple, but since that's immutable, as soon as ``@public`` tries to append to it, you will get an exception. Best practice is to not set ``__all__`` explicitly; let ``@public`` and ``@private`` do it! * If you still want ``__all__`` to be immutable, put the following at the bottom of your module:: __all__ = tuple(__all__) Alternatives ============ This isn't a unique approach to ``@public``. Other_ implementations_ do exist. There are some subtle differences between this package and those others. This package: * uses keyword arguments to map names which don't have an ``__name__`` attribute; * can be used to bind names and values into a module's globals; * can optionally put ``public`` in builtins. .. _`issue 26632`: http://bugs.python.org/issue26632 .. _builtins: https://docs.python.org/3/library/builtins.html .. _`directly controls`: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package .. _`actively prohibited`: http://pep8.readthedocs.io/en/latest/intro.html?highlight=e402#error-codes .. _`out of sync`: http://bugs.python.org/issue23883 .. _Other: https://pypi.python.org/pypi/public .. _implementations: http://bugs.python.org/issue22247#msg225637 .. _Sphinx: http://www.sphinx-doc.org/en/stable/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1618366883.9262772 atpublic-2.3/public/0000775000076500000240000000000000000000000013444 5ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618363452.0 atpublic-2.3/public/__init__.py0000664000076500000240000000077300000000000015564 0ustar00barrystafffrom .private import private from .public import public __version__ = '2.3' def install() -> None: """Install @public and @private into builtins.""" import builtins builtins.public = public # type: ignore [attr-defined] builtins.private = private # type: ignore [attr-defined] # mypy does not understand that __all__ gets populated at runtime via the # following call, __all__ = [ 'private', 'public', ] public( private=private, public=public, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618363452.0 atpublic-2.3/public/private.py0000664000076500000240000000136700000000000015477 0ustar00barrystaffimport sys from .types import ModuleAware def private(thing: ModuleAware) -> ModuleAware: """Remove names from __all__ This decorator documents private names and ensures that the names do not appear in the module's __all__. :param thing: An object with both a __module__ and a __name__ argument. :return: The original `thing` object. :raises ValueError: When this function finds a non-list __all__ attribute. """ mdict = sys.modules[thing.__module__].__dict__ dunder_all = mdict.setdefault('__all__', []) if not isinstance(dunder_all, list): raise ValueError(f'__all__ must be a list not: {type(dunder_all)}') if thing.__name__ in dunder_all: dunder_all.remove(thing.__name__) return thing ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618363452.0 atpublic-2.3/public/public.py0000664000076500000240000000576100000000000015305 0ustar00barrystaffimport sys from typing import Any, overload from .types import ModuleAware @overload def public(thing: ModuleAware) -> ModuleAware: ... # pragma: no cover @overload def public(**kws: Any) -> None: ... # pragma: no cover def public(thing=None, **kws): # type: ignore """Add a name or names to __all__ There are two forms of use for this function. Most commonly it will be used as a decorator on a class or function at module scope. In this case, `thing` will be an object with both __module__ and __name__ attributes, and the name is added to the module's __all__ list, creating that if necessary. When used in its function call form, `thing` will be None. __all__ is looked up in the globals at the function's call site, and each key in the keyword arguments is added to the __all__. In addition, the key will be bound to the value in the globals. Only one or the other format may be used. :param thing: None, or an object with both a __module__ and a __name__ argument. :param kws: Keyword arguments. :return: The original `thing` object. :raises ValueError: When the inputs are invalid, or this function finds a non-list __all__ attribute. """ # 2020-07-14(warsaw): I considered using inspect.getmodule() here but # looking at its implementation, I feel like it does a ton of unnecessary # work in the oddball cases (i.e. where the object does not have an # __module__ attribute). Because @public runs at module import time, and # because I'm not really sure we even want to support those oddball cases, # I'm taking the more straightforward approach of just looking the module # up in sys.modules. That should be good enough for our purposes. mdict = ( # The function call syntax. sys._getframe(1).f_globals if thing is None # The decorator syntax. else sys.modules[thing.__module__].__dict__ ) dunder_all = mdict.setdefault('__all__', []) if not isinstance(dunder_all, list): raise ValueError(f'__all__ must be a list not: {type(dunder_all)}') if thing is None: # The function call form. for key, value in kws.items(): # This overwrites any previous similarly named __all__ entry. if key not in dunder_all: dunder_all.append(key) # We currently do not check for duplications in the globals. mdict[key] = value else: # I think it's impossible to use the @public decorator and pass in # keyword arguments. Not quite syntactically impossible, but you'll # get a TypeError if you try it, before you even get to this code. assert ( len(kws) == 0 ), 'Keyword arguments are incompatible with use as decorator' if thing.__name__ not in dunder_all: dunder_all.append(thing.__name__) return thing ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1615265570.0 atpublic-2.3/public/py.typed0000664000076500000240000000000000000000000015131 0ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618363452.0 atpublic-2.3/public/types.py0000664000076500000240000000015300000000000015161 0ustar00barrystafffrom typing import Any, Callable, TypeVar ModuleAware = TypeVar('ModuleAware', bound=Callable[..., Any]) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1618366884.054145 atpublic-2.3/setup.cfg0000664000076500000240000000226700000000000014016 0ustar00barrystaff[tool:pytest] addopts = --cov=public testpaths = test docs [flake8] exclude = conf.py jobs = 1 max-line-length = 79 [coverage:report] fail_under = 100 show_missing = true [coverage:run] branch = true parallel = true omit = [coverage:paths] source = public [tool:isort] include_trailing_comma = true known_first_party = public length_sort_straight = true lines_after_imports = 2 lines_between_types = 1 multi_line_output = 3 order_by_type = false skip = conf.py [mypy] namespace_packages = true disallow_any_generics = true disallow_subclassing_any = true disallow_untyped_calls = false disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = false no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_return_any = true warn_unreachable = true implicit_reexport = false strict_equality = true show_error_context = true show_column_numbers = true show_error_codes = true pretty = true show_absolute_path = true warn_unused_configs = true verbosity = 0 [mypy-pytest] ignore_missing_imports = true [mypy-sybil.*] ignore_missing_imports = true [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1615265570.0 atpublic-2.3/setup.py0000664000076500000240000000336400000000000013706 0ustar00barrystaff"""public -- @public for populating __all__""" from setup_helpers import get_version, require_python from setuptools import setup, find_packages require_python(0x30600f0) __version__ = get_version('public/__init__.py') with open('README.rst') as fp: readme = fp.read() setup( name='atpublic', version=__version__, author='Barry Warsaw', author_email='barry@python.org', description=__doc__, long_description=readme, long_description_content_type='text/x-rst', license='Apache 2.0', keywords='__all__ public', url='http://public.readthedocs.io/', packages=find_packages(where='.', exclude=['test*', 'docs']), include_package_data=True, package_data={ 'public': ['public/py.typed'], }, # readthedocs builds fail unless zip_safe is False. zip_safe=False, python_requires='>=3.6', install_requires=[ 'typing_extensions;python_version<"3.8"', ], project_urls={ 'Documentation': 'https://public.readthedocs.io', 'Source': 'https://gitlab.com/warsaw/public.git', 'Tracker': 'https://gitlab.com/warsaw/public/issues', }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Utilities', ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1505519229.0 atpublic-2.3/setup_helpers.py0000664000076500000240000001210300000000000015417 0ustar00barrystaff# Copyright (C) 2009-2017 Barry Warsaw # # This file is part of setup_helpers.py # # setup_helpers.py is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, version 3 of the License. # # setup_helpers.py is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License # along with setup_helpers.py. If not, see . """setup.py helper functions.""" from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ 'description', 'find_doctests', 'get_version', 'long_description', 'require_python', ] import os import re import sys DEFAULT_VERSION_RE = re.compile( r'(?P\d+\.\d+(?:\.\d+)?(?:(?:a|b|rc)\d+)?)') EMPTYSTRING = '' __version__ = '2.3' def require_python(minimum): """Require at least a minimum Python version. The version number is expressed in terms of `sys.hexversion`. E.g. to require a minimum of Python 2.6, use:: >>> require_python(0x206000f0) :param minimum: Minimum Python version supported. :type minimum: integer """ if sys.hexversion < minimum: hversion = hex(minimum)[2:] if len(hversion) % 2 != 0: hversion = '0' + hversion split = list(hversion) parts = [] while split: parts.append(int(''.join((split.pop(0), split.pop(0))), 16)) major, minor, micro, release = parts if release == 0xf0: print('Python {0}.{1}.{2} or better is required'.format( major, minor, micro)) else: print('Python {0}.{1}.{2} ({3}) or better is required'.format( major, minor, micro, hex(release)[2:])) sys.exit(1) def get_version(filename, pattern=None): """Extract the __version__ from a file without importing it. While you could get the __version__ by importing the module, the very act of importing can cause unintended consequences. For example, Distribute's automatic 2to3 support will break. Instead, this searches the file for a line that starts with __version__, and extract the version number by regular expression matching. By default, two or three dot-separated digits are recognized, but by passing a pattern parameter, you can recognize just about anything. Use the `version` group name to specify the match group. :param filename: The name of the file to search. :type filename: string :param pattern: Optional alternative regular expression pattern to use. :type pattern: string :return: The version that was extracted. :rtype: string """ if pattern is None: cre = DEFAULT_VERSION_RE else: cre = re.compile(pattern) with open(filename) as fp: for line in fp: if line.startswith('__version__'): mo = cre.search(line) assert mo, 'No valid __version__ string found' return mo.group('version') raise AssertionError('No __version__ assignment found') def find_doctests(start='.', extension='.rst'): """Find separate-file doctests in the package. This is useful for Distribute's automatic 2to3 conversion support. The `setup()` keyword argument `convert_2to3_doctests` requires file names, which may be difficult to track automatically as you add new doctests. :param start: Directory to start searching in (default is cwd) :type start: string :param extension: Doctest file extension (default is .txt) :type extension: string :return: The doctest files found. :rtype: list """ doctests = [] for dirpath, dirnames, filenames in os.walk(start): doctests.extend(os.path.join(dirpath, filename) for filename in filenames if filename.endswith(extension)) return doctests def long_description(*filenames): """Provide a long description.""" res = [''] for filename in filenames: with open(filename) as fp: for line in fp: res.append(' ' + line) res.append('') res.append('\n') return EMPTYSTRING.join(res) def description(filename): """Provide a short description.""" # This ends up in the Summary header for PKG-INFO and it should be a # one-liner. It will get rendered on the package page just below the # package version header but above the long_description, which ironically # gets stuff into the Description header. It should not include reST, so # pick out the first single line after the double header. with open(filename) as fp: for lineno, line in enumerate(fp): if lineno < 3: continue line = line.strip() if len(line) > 0: return line ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1618366884.0523407 atpublic-2.3/test/0000775000076500000240000000000000000000000013145 5ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603406871.0 atpublic-2.3/test/__init__.py0000664000076500000240000000000000000000000015244 0ustar00barrystaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618339235.0 atpublic-2.3/test/test_mypy.py0000664000076500000240000000031400000000000015552 0ustar00barrystaff# https://gitlab.com/warsaw/public/-/issues/10 from public import private, public @public def one(x: int) -> int: return x * 2 one(4) @private def two(x: int) -> int: return x * 3 two(4) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603406871.0 atpublic-2.3/test/test_private.py0000664000076500000240000000163300000000000016233 0ustar00barrystafffrom importlib import import_module import pytest def test_atprivate(example): example("""\ from public import private @private def a_function(): pass """) module = import_module('example') assert 'a_function' not in module.__all__ def test_atprivate_with_dunder_all(example): example("""\ from public import private __all__ = ['a_function'] @private def a_function(): pass """) module = import_module('example') assert 'a_function' not in module.__all__ def test_atprivate_adds_dunder_all(example): example("""\ from public import private @private def a_function(): pass """) module = import_module('example') assert module.__all__ == [] def test_all_is_a_tuple(example): example("""\ __all__ = ('foo',) from public import private def foo(): pass @private def bar(): pass """) with pytest.raises(ValueError): import_module('example') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1603406871.0 atpublic-2.3/test/test_public.py0000664000076500000240000000604000000000000016034 0ustar00barrystaffimport builtins from importlib import import_module import pytest from public import install def test_atpublic_function(example): example("""\ from public import public @public def a_function(): pass """) module = import_module('example') assert module.__all__ == ['a_function'] def test_atpublic_function_runnable(example): example("""\ from public import public @public def a_function(): return 1 """) module = import_module('example') assert module.a_function() == 1 def test_atpublic_class(example): example("""\ from public import public @public class AClass: pass """) module = import_module('example') assert module.__all__ == ['AClass'] def test_atpublic_class_runnable(example): example("""\ from public import public @public class AClass: pass """) module = import_module('example') assert isinstance(module.AClass(), module.AClass) def test_atpublic_two_things(example): example("""\ from public import public @public def foo(): pass @public class AClass: pass """) module = import_module('example') assert module.__all__ == ['foo', 'AClass'] def test_decorator_duplicate(example): example("""\ from public import public @public def foo(): return 1 @public def foo(): return 2 """) module = import_module('example') assert module.__all__ == ['foo'] def test_function_call_duplicate(example): example("""\ from public import public @public def foo(): return 1 public(foo=2) """) module = import_module('example') assert module.__all__ == ['foo'] def test_atpublic_append_to_all(example): example("""\ __all__ = ['a', 'b'] a = 1 b = 2 from public import public @public def foo(): pass @public class AClass: pass """) module = import_module('example') assert module.__all__ == ['a', 'b', 'foo', 'AClass'] def test_atpublic_keywords(example): example("""\ from public import public public(a=1, b=2) """) module = import_module('example') assert sorted(module.__all__) == ['a', 'b'] def test_atpublic_keywords_multicall(example): example("""\ from public import public public(b=1) public(a=2) """) module = import_module('example') assert module.__all__ == ['b', 'a'] def test_atpublic_keywords_global_bindings(example): example("""\ from public import public public(a=1, b=2) """) module = import_module('example') assert module.a == 1 assert module.b == 2 def test_atpublic_mixnmatch(example): example("""\ __all__ = ['a', 'b'] a = 1 b = 2 from public import public @public def foo(): pass @public class AClass: pass public(c=3) """) module = import_module('example') assert module.__all__ == ['a', 'b', 'foo', 'AClass', 'c'] def test_all_is_a_tuple(example): example("""\ __all__ = ('foo',) from public import public def foo(): pass @public def bar(): pass """) with pytest.raises(ValueError): import_module('example') def test_install(): try: install() finally: delattr(builtins, 'public') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1618339235.0 atpublic-2.3/tox.ini0000664000076500000240000000114200000000000013477 0ustar00barrystaff[tox] envlist = {py36,py37,py38,py39},qa,docs skip_missing_interpreters = True [testenv] commands = python -m pytest {posargs} usedevelop = True setenv = PYTHONPATH = '' deps = pytest pytest-cov sybil passenv = ATPUBLIC_* PYTHON* [testenv:qa] basepython = python3 commands = python -m flake8 public isort public blue --diff public mypy -p public mypy test/test_mypy.py deps = flake8 isort>=5.4.1 mypy blue>=0.6.0 [testenv:docs] basepython = python3 commands = python setup.py build_sphinx -c docs deps: sphinx sphinx_autodoc_typehints