pax_global_header00006660000000000000000000000064151124757170014524gustar00rootroot0000000000000052 comment=74cd225d884c8baa64087e226be97f15d35743d9 public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/000077500000000000000000000000001511247571700203275ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/.bandit000066400000000000000000000000571511247571700215730ustar00rootroot00000000000000[bandit] exclude: setup_helpers.py conftest.py public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/.gitignore000066400000000000000000000002101511247571700223100ustar00rootroot00000000000000*.so *.egg-info build .coverage dist htmlcov coverage.xml diffcov.html /.pdm.toml /__pypackages__/ /.DS_Store /.pdm-python /.coverage.* public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/.gitlab-ci.yml000066400000000000000000000002371511247571700227650ustar00rootroot00000000000000include: - remote: https://gitlab.com/warsaw/gitlab-ci/-/raw/main/common-gitlab-ci.yml variables: MODULE_NAME: "public" MODULE_PATH: "src/public" public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/.readthedocs.yml000066400000000000000000000004701511247571700234160ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-24.04 tools: python: "latest" jobs: install: - pip install --upgrade pip - pip install -e . --group docs sphinx: configuration: docs/conf.py public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/LICENSE000066400000000000000000000010541511247571700213340ustar00rootroot00000000000000Copyright 2016-2025 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. public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/README.rst000066400000000000000000000015551511247571700220240ustar00rootroot00000000000000====================== @public and @private ====================== This library provides two very simple decorators that document the public visibility of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Also included is a function that you can put at the bottom of your module to simply infer all the public names, and populate the ``__all__`` for you. Author ====== ``public`` is Copyright (C) 2016-2025 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 public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/conftest.py000066400000000000000000000102061511247571700225250ustar00rootroot00000000000000import os import sys from importlib import import_module try: # Python < 3.12 from importlib_resources import files except ImportError: from importlib.resources import files from contextlib import ExitStack, contextmanager from doctest import ELLIPSIS, REPORT_NDIFF, NORMALIZE_WHITESPACE from sybil import Sybil from sybil.parsers.codeblock import PythonCodeBlockParser 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) def import_example(filename): # This assumes the file is relative to the docs/ directory. path = files() / 'docs' / filename with path.open(encoding='utf-8') as fp: contents = fp.read() 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') with open(path, 'w', encoding='utf-8') as fp: fp.write(contents) return import_module('example') 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 # Used in the doctests to provide a clean __all__. def reset(): self._testmod.__all__ = namespace['__all__'] = [] reset() namespace['reset'] = reset namespace['import_example'] = import_example def teardown(self, namespace): del sys.modules[self._testmod.__name__] namespace = DoctestNamespace() pytest_collect_file = Sybil( parsers=[ DocTestParser(optionflags=DOCTEST_FLAGS), PythonCodeBlockParser(), ], pattern='*.rst', setup=namespace.setup, ).pytest() public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/000077500000000000000000000000001511247571700212575ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/NEWS.rst000066400000000000000000000123771511247571700225770ustar00rootroot00000000000000================== @public change log ================== 7.0.0 ===== * Drop Python 3.9. * Documentation improvements. `furo` has been fixed, so we no longer need the workaround. * ``pyproject.toml`` updates and improvements. 6.0.2 (2025-09-24) ================== * Documentation improvements. * Temporary `workaround `__ for a furo theme bug in dark mode. * CI updates. 6.0.1 (2025-05-06) ================== * Fix test suite when run with Python < 3.12. (:GL:`28`) * Add test for Python 3.14. 6.0 (2025-05-06) ================ * Added ``populate_all()`` which can be called from the bottom of your module to infer and populate your module's ``__all__``. Use this if you don't like the decorator syntax. (:GL:`27`) * Documentation improvements. 5.1 (2025-01-23) ================ * Drop official support for Python 3.8, add support for Python 3.13. 5.0 (2024-07-24) ================ * ``@public`` is now properly type annotated. * Adopt ``hatch test`` and ``hatch fmt`` commands. (:GL:`25`) * ``@public`` and ``@private`` now raise ``TypeError`` instead of ``ValueError`` if ``__all__`` is not a concrete ``list`` object. (:GL:`26`) * Other minor coding improvements identified by ``ruff``. * Switch to `Trusted Publishing `_ for publishing new versions to PyPI. (:GL:`24`) 4.1 (2024-03-29) ================ * Add support for Python 3.12. (:GL:`22`) * Switch to ``hatch``, replacing ``pdm`` and ``tox``. (:GL:`21`) 4.0 (2023-06-05) ================ * Drop Python 3.7 support. (:GL:`16`) * Remove ``public.install()`` which was used to inject the ``public`` and ``private`` functions into the ``builtins`` namespace. This isn't very helpful and could be actively harmful. Explicit is better than implicit. (:GL:`14`) * The functional form of ``public()`` now returns the argument *values* in the order they are given. This allows you to explicitly bind those values to names in the global namespace. While this is redundant, it does solve some linter problems. (:GL:`12`) * Switch from ``flake8`` and ``isort`` to ``ruff`` for code quality. (:GL:`32`) * Bump dependencies. 3.1.2 (2023-05-31) ================== * Switch to ``pdm-backend`` (:GL:`15`) * Bump dependencies. * More GitLab CI integration improvements. 3.1.1 (2022-09-02) ================== * Improvements to the GitLab CI integration. 3.1 (2022-08-27) ================ * Fix a typo in pyproject.toml file. * Exclude certain local cache files from the sdist/wheel. * Add support for Python 3.11. * Updates for pdm and dependencies. 3.0.1 (2022-01-10) ================== * Fix a typo in the README.rst. 3.0 (2022-01-10) ================ * Use modern package management by adopting `pdm `_ and ``pyproject.toml``, and dropping ``setup.py`` and ``setup.cfg``. * Build the docs with Python 3.8. * Update to version 3.0 of `Sybil `_. * Adopt the `Furo `_ documentation theme. * Use `importlib.metadata.version() `_ as a better way to get the package version number for the documentation. * Drop Python 3.6 support. * Update Windows GitLab runner to include Python 3.10. * Update copyright years. * The ``master`` branch is renamed to ``main``. (:GL:`11`) 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. public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/__init__.py000066400000000000000000000000001511247571700233560ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/_ext/000077500000000000000000000000001511247571700222165ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/_ext/issue_role.py000066400000000000000000000024621511247571700247450ustar00rootroot00000000000000from docutils import nodes from sphinx.application import Sphinx from sphinx.roles import ReferenceRole from sphinx.util.typing import ExtensionMetadata BASE_URL = 'https://gitlab.com/warsaw/public/-/issues/' class IssueRole(ReferenceRole): """A role to hyperlink GitLab issues. Use like this: :GL:`16` """ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]: try: issue_number = int(self.target) except ValueError: message = self.inliner.reporter.error( f'Role target must be an integer :GL:{self.target}' ) problem = self.inliner.problematic(self.rawtext, self.rawtext, message) return [problem], [message] issue_uri = BASE_URL + self.target title = self.title if self.has_explicit_title else f'GL#{self.target}' return [ nodes.reference( '', title, internal=True, refuri=issue_uri, classes=['issue'], _title_tuple=(issue_number,), ) ], [] def setup(app: Sphinx) -> ExtensionMetadata: app.add_role('GL', IssueRole()) return { 'version': '0.1', 'parallel_read_safe': True, 'parallel_write_safe': True, } public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/apiref.rst000066400000000000000000000002641511247571700232610ustar00rootroot00000000000000============= API Reference ============= API reference for ``public``: .. autofunction:: public.public .. autofunction:: public.private .. autofunction:: public.populate_all public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/conf.py000066400000000000000000000164711511247571700225670ustar00rootroot00000000000000# -*- 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 from datetime import date import importlib.metadata # 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[0:0] = [ os.path.abspath('../src'), os.path.abspath('_ext'), ] # -- 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 = [ 'issue_role', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx_copybutton', ] intersphinx_mapping = { 'python': ('https://docs.python.org/', None), } autodoc_typehints = 'both' copybutton_exclude = '.linenos, .gp, .go' # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = {'.rst': 'restructuredtext'} # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'public' author = 'Barry Warsaw' copyright = f'2004-{date.today().year}, {author}' # 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. version = importlib.metadata.version('atpublic') # 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 = 'furo' ## html_favicon = '_static/lock-light.svg' ## html_theme_options = { ## 'light_logo': 'logo-light.png', ## 'dark_logo': 'logo-dark.png', ## } # 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', 'public Documentation', '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', 'public Documentation', ['Barry Warsaw'], 1)] public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/index.rst000066400000000000000000000044551511247571700231300ustar00rootroot00000000000000========================================= Documenting the public interface ========================================= .. currentmodule:: public This library provides two decorators that document the public visibility of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Also included is a function that you can put at the bottom of your module to simply infer all the public names, and populate the ``__all__`` for you. 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.10 or newer. Documentation ============= More information is available in the :doc:`user guide ` and the :doc:`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``: .. code-block:: console $ pip install atpublic .. attention:: Do not install ``public``; that is a different package! You can grab the latest development copy of the code using git. The main repository is hosted on GitLab. If you have git installed, you can grab your own branch of the code like this: .. code-block:: console $ git clone https://gitlab.com/warsaw/public.git You can contact the author via barry@python.org. Copyright ========= Copyright (C) 2016-2025 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 public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/popall_example.py000066400000000000000000000002171511247571700246330ustar00rootroot00000000000000# example.py from public import populate_all def foo(): pass class Foo: pass fooint: int = 7 _foobool: bool = False populate_all() public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/docs/using.rst000066400000000000000000000242431511247571700231430ustar00rootroot00000000000000================================== Documenting the public interface ================================== This library provides two decorators that document the public visibility of the names in your module. They keep your module's ``__all__`` in sync so you don't have to. Also included is a function that you can put at the bottom of your module to simply infer all the public names, and populate the ``__all__`` for you. 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 with __all__ ======================== ``__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! @public ======= This package provides a way to declare a name's public visibility 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()`` function that can be used as either a decorator, or a callable. .. code-block:: pycon >>> from public import public You'll usually use this as a decorator, for example: .. code-block:: pycon >>> @public ... def foo(): ... pass or: .. code-block:: pycon >>> @public ... class Bar: ... pass The ``__all__`` after both of those code snippets has both names in it: .. code-block:: pycon >>> print(__all__) ['foo', 'Bar'] .. note:: 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 append any new names to the existing list. Function call form ================== 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 almost 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. .. invisible-code-block: pycon >>> reset() .. code-block:: pycon >>> public(SEVEN=7) 7 >>> public(a_bar=Bar()) <...Bar object ...> The module's ``__all__`` now contains both of the keys: .. code-block:: pycon >>> print(__all__) ['SEVEN', 'a_bar'] and as should be obvious, the module contains name bindings for these constants: .. code-block:: pycon >>> print(SEVEN) 7 >>> print(a_bar) <....Bar object at ...> Multiple keyword arguments are allowed: .. code-block:: pycon >>> public(ONE=1, TWO=2) (1, 2) >>> print(__all__) ['SEVEN', 'a_bar', 'ONE', 'TWO'] >>> print(ONE) 1 >>> print(TWO) 2 You'll notice that the functional form of ``public()`` returns the values in its keyword arguments in order. This is to help with a use case where some linters complain because they can't see that ``public()`` binds the names in the global namespace. In the above example they might report erroneously that ``ONE`` and ``TWO`` aren't defined. To work around this, when ``public()`` is used in its functional form, it will return the values in the order they are seen [#]_ and you can simply assign them to explicit local variable names. .. code-block:: pycon >>> a, b, c = public(a=3, b=2, c=1) >>> print(__all__) ['SEVEN', 'a_bar', 'ONE', 'TWO', 'a', 'b', 'c'] >>> print(a, b, c) 3 2 1 It also works if you bind only a single value. .. code-block:: pycon >>> d = public(d=9) >>> print(__all__) ['SEVEN', 'a_bar', 'ONE', 'TWO', 'a', 'b', 'c', 'd'] >>> print(d) 9 @private ======== You might also want to be explicit about your private, i.e. non-public, names. This library 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__``. .. invisible-code-block: pycon >>> reset() .. code-block:: pycon >>> from public import private >>> @public ... def foo(): pass >>> print(__all__) ['foo'] >>> @private ... def foo(): pass >>> print(__all__) [] 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: .. invisible-code-block: pycon >>> reset() .. code-block:: pycon >>> @private ... class Baz: ... pass >>> print(__all__) [] In this case, ``Baz`` never appears in ``__all__``. Like with ``@public``, the ``@private`` decorator will initialize ``__all__`` if needed, but if it exists in the module, it must be a list. There is no functional API for ``@private``. Inferring __all__ ================= If you don't like using the decorators, you can instead infer and populate the contents of ``__all__`` by calling the ``populate_all()`` function at the bottom of your module. This uses heuristics to pick out some names from the module, adding them to ``__all__`` if they meet the following criteria: * The name does not start with an underscore. * The name is not bound to a module object. This prevents imported modules from being added. * The object the name is bound to does not appear to be defined in some other module. This prevents most from-imports from being added, but note that this can be fooled if you import simple types (such as an ``int`` or a ``str``) from another module (e.g. ``from sys import abiflags``), because simple types don't have a ``__module__`` attribute. For example, if your Python module looks like this: .. literalinclude:: popall_example.py :language: python :linenos: when you import this module, the ``__all__`` will be populated with the names matching the above heuristics. .. invisible-code-block: pycon >>> example = import_example('popall_example.py') .. code-block:: pycon >>> example.__all__ ['foo', 'Foo', 'fooint'] In this case, you can see that the module has an ``__all__`` set to ``['foo', 'Foo', 'fooint']`` but note that neither ``_foobool`` nor ``populate_all`` are added. If the inferencing misses some names you want to publicly export, you can always add them explicitly by using ``@public`` or appending to ``__all__``. .. note:: This function only adds new names to ``__all__``. 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: .. code-block:: python __all__ = tuple(__all__) .. [#] This is ordering is guaranteed by `PEP 468 `_. .. _`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 .. _implementations: http://bugs.python.org/issue22247#msg225637 .. _Sphinx: http://www.sphinx-doc.org/en/stable/ public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/pyproject.toml000066400000000000000000000115421511247571700232460ustar00rootroot00000000000000[project] name = 'atpublic' authors = [ {name = 'Barry Warsaw', email = 'barry@python.org'}, ] description = "Keep all y'all's __all__'s in sync" readme = 'README.rst' requires-python = '>=3.10' license = {text = 'Apache-2.0'} keywords = [ '__all__', 'public', 'private', ] 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', ] dependencies = [] dynamic = ['version'] [dependency-groups] docs = [ 'furo', 'sphinx', 'sphinx-autobuild', 'sphinx-copybutton', ] [project.urls] 'Home Page' = 'https://public.readthedocs.io' 'Documentation' = 'https://public.readthedocs.io' 'Source' = 'https://gitlab.com/warsaw/public.git' 'Bug Tracker' = 'https://gitlab.com/warsaw/public/issues' [tool.hatch.version] path = 'src/public/__init__.py' [tool.hatch.build.targets.wheel] packages = [ 'src/public', ] [tool.hatch.build.targets.sdist] include = [ 'src/public/', 'docs/', 'tests/', 'conftest.py', ] excludes = [ '**/.mypy_cache', ] [tool.hatch.envs.default] installer = 'uv' [tool.hatch.envs.default.scripts] all = [ 'hatch test --all', 'hatch run qa:qa', 'hatch run docs:docs', ] [tool.hatch.envs.hatch-test] default-args = ['tests', 'docs'] extra-dependencies = [ 'diff-cover', 'importlib_resources >= 5.10; python_version < "3.12"', 'sybil', ] [tool.hatch.envs.hatch-test.scripts] run = [ 'coverage run -m pytest{env:HATCH_TEST_ARGS:} {args}', 'coverage combine', 'coverage report', 'coverage xml', '- diff-cover coverage.xml', ] run-cov = 'hatch test' cov-combine = '' cov-report = '' [[tool.hatch.envs.hatch-test.matrix]] python = ['3.10', '3.11', '3.12', '3.13', '3.14'] [tool.hatch.envs.qa] dependencies = [ 'ruff', 'mypy', ] [tool.hatch.envs.qa.env-vars] MODULE_NAME = '{env:MODULE_NAME:public}' MODULE_PATH = '{env:MODULE_PATH:src/public}' [tool.hatch.envs.qa.scripts] qa = [ 'hatch fmt --check src', 'mypy -p {env:MODULE_NAME}', ] fix = [ 'hatch fmt src', ] preview = [ 'hatch fmt --diff src', ] [tool.hatch.envs.hatch-static-analysis] config-path = 'none' [tool.hatch.envs.docs] dependency-groups = [ 'docs', ] [tool.hatch.envs.docs.scripts] docs = [ 'sphinx-build docs build/html', ] serve = [ 'sphinx-autobuild docs build/html --port 9000 --open-browser', ] [tool.coverage.run] source = ['public'] branch = true parallel = true [tool.coverage.report] fail_under = 100 show_missing = true exclude_also = [ 'if TYPE_CHECKING:', '^\s*\.{3}$', # Ignore ... for @overload ] [tool.ruff] exclude = [ 'docs/popall_example.py', ] line-length = 100 src = ['src'] [tool.ruff.lint.extend-per-file-ignores] # Essentially, ignore all lint warnings in these configuration files. 'conftest.py' = [ 'ARG002', 'I001', 'S101', ] 'docs/conf.py' = [ 'A', 'DTZ', 'E', 'I', 'UP', ] 'docs/_ext/**.py' = [ 'INP001', ] 'src/public/**.py' = [ 'TID', # Allow relative imports ] 'src/public/public.py' = [ 'S101', # Allow assert 'SLF001', # Allow sys._getframe() ] [tool.ruff.format] quote-style = 'single' [tool.ruff.lint.pydocstyle] convention = 'pep257' [tool.ruff.lint.isort] case-sensitive = true forced-separate = ['docutils', 'sphinx'] length-sort-straight = true lines-after-imports = 2 lines-between-types = 1 order-by-type = true section-order = ['standard-library', 'third-party', 'local-folder', 'first-party'] [tool.mypy] mypy_path = 'src' # Disallow dynamic typing disallow_any_generics = true disallow_subclassing_any = true # Untyped definitions and calls disallow_untyped_calls = false disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = false # None and Optional handling no_implicit_optional = true # Configuring warnings warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_return_any = true warn_unreachable = true # Miscellaneous strictness flags implicit_reexport = false strict_equality = true # Configuring error messages show_error_context = true show_column_numbers = true show_error_codes = true pretty = true show_absolute_path = true # Miscellaneous warn_unused_configs = true verbosity = 0 [[tool.mypy.overrides]] module = [ 'pytest', 'sybil.*', ] ignore_missing_imports = true [build-system] requires = ['hatchling'] build-backend = 'hatchling.build' public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/000077500000000000000000000000001511247571700211165ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/000077500000000000000000000000001511247571700223745ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/__init__.py000066400000000000000000000005671511247571700245150ustar00rootroot00000000000000from .modules import populate_all from .private import private from .public import public __version__ = '7.0.0' # mypy does not understand that __all__ gets populated at runtime via the # public() call below, so be explicit. __all__ = [ 'populate_all', 'private', 'public', ] public( populate_all=populate_all, private=private, public=public, ) public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/modules.py000066400000000000000000000042711511247571700244220ustar00rootroot00000000000000def populate_all() -> None: """Populate the calling module's __all__ with all of its public names. The criteria for what is a public name is derived from various common linter rules, although it's not an exact science. * The name does not start with an underscore. * The name is not bound to a module object. This prevents imported modules from being added. * The object the name is bound to does not appear to be defined in some other module. This prevents most from-imports from being added, but note that this can be fooled if you import simple types (such as an ``int`` or a ``str``) from another module (e.g. ``from sys import abiflags``), because simple types don't have a ``__module__`` attribute. If you find that some names are missing from the list, you can add them to __all__ explicitly by using the @public decorator. If you find some names in __all__ that should not be present, decorate them with the @private decorator. This function respects any existing __all__ in your module. Typical usage is to call this function at the bottom of your module. """ # Import this here rather than at module scope so we don't pay the import cost just to export # this function in the `public` package namespace. import inspect frameinfo_called_in = inspect.stack()[1] if (module := inspect.getmodule(frameinfo_called_in.frame)) is None: return mdict = frameinfo_called_in.frame.f_globals dunder_all = mdict.setdefault('__all__', []) seen = set(dunder_all) for name, binding in mdict.items(): if name.startswith('_'): continue if inspect.ismodule(binding): continue # Don't export objects imported from other modules. Simple types won't have a module. Note # that simple types imported with `from import module *` will show up in __all__. if inspect.getmodule(binding) not in (module, None): continue # We don't want to add names to the list twice, but we do want to preserve the order of any # existing __all__ strings. if name not in seen: dunder_all.append(name) seen.add(name) public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/private.py000066400000000000000000000015141511247571700244210ustar00rootroot00000000000000import 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): # https://docs.astral.sh/ruff/rules/f-string-in-exception/ msg = f'__all__ must be a list not: {type(dunder_all)}' raise TypeError(msg) if thing.__name__ in dunder_all: dunder_all.remove(thing.__name__) return thing public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/public.py000066400000000000000000000077261511247571700242400ustar00rootroot00000000000000# https://docs.astral.sh/ruff/rules/future-rewritable-type-annotation/ # # 2024-05-02(bwarsaw): We must ignore I001 on this line or the ruff formatter # and linter will be in conflict between I001 and F404 (which wants to move # this import to below `import sys`. Ruff's unified linter and formatter # will hopefully resolve this: https://github.com/astral-sh/ruff/issues/8232 from __future__ import annotations # noqa: I001 import sys from typing import Any, TYPE_CHECKING, overload if TYPE_CHECKING: from .types import ModuleAware # fmt: off @overload def public(thing: ModuleAware) -> ModuleAware: ... @overload def public(**kws: Any) -> Any | tuple[Any]: ... # fmt: on def public(thing: Any | None = None, **kws: Any) -> ModuleAware | Any | tuple[Any]: """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. This form returns the keyword argument values in order. If only a single keyword argument is given, its value is return, otherwise a tuple of the values is returned. 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: In the decorator form, the original ``thing`` object is returned. In the functional form, the keyword argument value is returned if only a single keyword argument is given, otherwise a tuple of the keyword argument values is returned. :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): # https://docs.astral.sh/ruff/rules/f-string-in-exception/ msg = f'__all__ must be a list not: {type(dunder_all)}' raise TypeError(msg) if thing is None: # The function call form. retval = [] 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 retval.append(value) if len(retval) == 1: return retval[0] return tuple(retval) # 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 public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/py.typed000066400000000000000000000000001511247571700240610ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/src/public/types.py000066400000000000000000000001531511247571700241110ustar00rootroot00000000000000from typing import Any, Callable, TypeVar ModuleAware = TypeVar('ModuleAware', bound=Callable[..., Any]) public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/tests/000077500000000000000000000000001511247571700214715ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/tests/__init__.py000066400000000000000000000000001511247571700235700ustar00rootroot00000000000000public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/tests/test_modules.py000066400000000000000000000044611511247571700245570ustar00rootroot00000000000000from importlib import import_module import pytest def test_populate_all(example): example("""\ from public import populate_all def foo(): pass class Foo: pass fooint: int = 7 foostr: str = 'hello' # This isn't public. _foobool: bool = True populate_all() """) module = import_module('example') assert module.__all__ == ['foo', 'Foo', 'fooint', 'foostr'] def test_populate_all_preserves_all(example): example("""\ from public import populate_all __all__ = ['missing'] def foo(): pass populate_all() """) module = import_module('example') assert module.__all__ == ['missing', 'foo'] def test_populate_all_skips_duplicates(example): example("""\ from public import populate_all __all__ = ['foo'] def foo(): pass populate_all() """) module = import_module('example') assert module.__all__ == ['foo'] def test_populate_all_skips_decorated_duplicates(example): example("""\ from public import populate_all, public @public def foo(): pass populate_all() """) module = import_module('example') assert module.__all__ == ['foo'] def test_populate_all_skips_modules(example): example("""\ import public public.populate_all() """) module = import_module('example') assert module.__all__ == [] def test_populate_all_noop_unsupported_getmodule(example, monkeypatch): # inspect.getmodule() is documented as possibly returning None, and the code is prepared for # that, but I don't know under what circumstances that can occur, so just monkeypatch the # function for full coverage. # # https://docs.python.org/3/library/inspect.html#inspect.getmodule monkeypatch.setattr('inspect.getmodule', lambda *_: None) example("""\ from public import populate_all def foo(): pass populate_all() """) module = import_module('example') # There will be no __all__ because it wasn't added explicitly, and nothing could be inferred # because of the monkey patch. assert not hasattr(module, '__all__') # This can't pass because sys.abiflags is a string, for which inspect.getmodule() returns None. @pytest.mark.xfail def test_populate_all_gets_fooled(example): example("""\ from public import populate_all from sys import abiflags populate_all() """) module = import_module('example') assert module.__all__ == [] public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/tests/test_mypy.py000066400000000000000000000003141511247571700240760ustar00rootroot00000000000000# 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) public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/tests/test_private.py000066400000000000000000000016321511247571700245560ustar00rootroot00000000000000from 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(TypeError): import_module('example') public-7.0.0-74cd225d884c8baa64087e226be97f15d35743d9/tests/test_public.py000066400000000000000000000056201511247571700243630ustar00rootroot00000000000000from importlib import import_module import pytest 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(TypeError): import_module('example')