pax_global_header00006660000000000000000000000064146016664220014521gustar00rootroot0000000000000052 comment=f8aef51a6cda8ab9625fdcbfdf6f3caf26c67e17 public-4.1.0/000077500000000000000000000000001460166642200130015ustar00rootroot00000000000000public-4.1.0/.bandit000066400000000000000000000000571460166642200142450ustar00rootroot00000000000000[bandit] exclude: setup_helpers.py conftest.py public-4.1.0/.gitignore000066400000000000000000000001731460166642200147720ustar00rootroot00000000000000*.so *.egg-info build .coverage dist htmlcov coverage.xml diffcov.html /.pdm.toml /__pypackages__/ /.DS_Store /.pdm-python public-4.1.0/.gitlab-ci.yml000066400000000000000000000002431460166642200154340ustar00rootroot00000000000000include: - remote: https://gitlab.com/warsaw/gitlab-ci/-/raw/hatchify/common-gitlab-ci.yml variables: MODULE_NAME: "public" MODULE_PATH: "src/public" public-4.1.0/.readthedocs-req.txt000066400000000000000000000000361460166642200166710ustar00rootroot00000000000000sphinx_autodoc_typehints furo public-4.1.0/.readthedocs.yml000066400000000000000000000004641460166642200160730ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "latest" sphinx: configuration: "docs/conf.py" python: install: - requirements: .readthedocs-req.txt - method: pip path: . public-4.1.0/LICENSE000066400000000000000000000010541460166642200140060ustar00rootroot00000000000000Copyright 2016-2024 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-4.1.0/README.rst000066400000000000000000000013211460166642200144650ustar00rootroot00000000000000====================== @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-2024 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-4.1.0/conftest.py000066400000000000000000000065571460166642200152150ustar00rootroot00000000000000import 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 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) 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 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-4.1.0/docs/000077500000000000000000000000001460166642200137315ustar00rootroot00000000000000public-4.1.0/docs/NEWS.rst000066400000000000000000000075531460166642200152510ustar00rootroot00000000000000================== @public change log ================== 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-4.1.0/docs/__init__.py000066400000000000000000000000001460166642200160300ustar00rootroot00000000000000public-4.1.0/docs/apiref.rst000066400000000000000000000002151460166642200157270ustar00rootroot00000000000000============= API Reference ============= API reference for ``public``: .. autofunction:: public.public .. autofunction:: public.private public-4.1.0/docs/conf.py000066400000000000000000000162361460166642200152400ustar00rootroot00000000000000# -*- 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.insert(0, os.path.abspath('../src')) # -- 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', ] intersphinx_mapping = { 'python': ('https://docs.python.org/', None), } autodoc_typehints = 'both' # 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' 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', 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) ] public-4.1.0/docs/index.rst000066400000000000000000000043121460166642200155720ustar00rootroot00000000000000========================================================== @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.8 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 main 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 can contact the author via barry@python.org. Copyright ========= Copyright (C) 2016-2024 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 public-4.1.0/docs/using.rst000066400000000000000000000207331460166642200156150ustar00rootroot00000000000000========================== Using @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 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! 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. 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 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. We'll start by resetting the ``__all__``. >>> reset() >>> public(SEVEN=7) 7 >>> public(a_bar=Bar()) <...Bar object ...> The module's ``__all__`` now contains both of the keys:: >>> print(__all__) ['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) (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 bcause 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. >>> 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. >>> 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 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__``. As above, we'll start by resetting ``__all__``:: >>> reset() >>> from public import private >>> @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:: >>> @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``. 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. .. rubric:: Footnotes .. [#] 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 .. _Other: https://pypi.python.org/pypi/public .. _implementations: http://bugs.python.org/issue22247#msg225637 .. _Sphinx: http://www.sphinx-doc.org/en/stable/ public-4.1.0/pyproject.toml000066400000000000000000000113351460166642200157200ustar00rootroot00000000000000[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.8' 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'] [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/', 'test/', 'tox.ini', 'conftest.py', ] excludes = [ '*/.mypy_cache/', ] [tool.hatch.envs.default.scripts] all = [ 'hatch run test:test', 'hatch run qa:qa', 'hatch run docs:docs', ] [[tool.hatch.envs.test.matrix]] python = ['3.8', '3.9', '3.10', '3.11', '3.12'] [tool.hatch.envs.test] dependencies = [ 'coverage[toml]', 'diff-cover', 'pytest', 'pytest-cov', 'sybil', ] [tool.hatch.envs.test.scripts] test = [ 'pytest {args}', # The following is only useful in a git branch of main. '- diff-cover coverage.xml', ] [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 = [ 'ruff check {env:MODULE_PATH}', 'mypy -p {env:MODULE_NAME}', ] fix = [ 'ruff check --fix {env:MODULE_PATH}', ] [tool.hatch.envs.docs] dependencies = [ 'sphinx', 'furo', ] [tool.hatch.envs.docs.scripts] docs = [ 'sphinx-build docs build/html', ] [tool.pytest.ini_options] addopts = '--cov=public --cov-report=term --cov-report=xml -p no:doctest' testpaths = 'test docs' [tool.coverage.report] fail_under = 100 show_missing = true [tool.coverage.run] branch = true parallel = true [tool.ruff] line-length = 79 src = ['src'] [tool.ruff.lint] select = [ 'B', # flake8-bugbear 'D', # pydocstyle 'E', # pycodestyle 'F', # pyflakes 'I', 'RUF100', # check for valid noqa directives 'UP', # pyupgrade 'W', # pycodestyle ] ignore = [ 'D100', # Missing docstring in public module 'D104', # Missing docstring in public package ] [tool.ruff.lint.pydocstyle] convention = 'pep257' [tool.ruff.lint.isort] # 2023-06-04(warsaw): Some isort options are not yet supported by ruff. Also, # while isort supports order-by-type=false and case-sensitive=true to get the # preferred ordering of from-imports, ruff does not support this combination # (there's no case-sensitive option). We can achieve the same results by # setting order-by-type=true, although I suspect this won't do the right thing # in some of my other repos. # #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 = true [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-4.1.0/src/000077500000000000000000000000001460166642200135705ustar00rootroot00000000000000public-4.1.0/src/public/000077500000000000000000000000001460166642200150465ustar00rootroot00000000000000public-4.1.0/src/public/__init__.py000066400000000000000000000004421460166642200171570ustar00rootroot00000000000000from .private import private from .public import public __version__ = '4.1.0' # mypy does not understand that __all__ gets populated at runtime via the # public() call below, so be explicit. __all__ = [ 'private', 'public', ] public( private=private, public=public, ) public-4.1.0/src/public/private.py000066400000000000000000000013701460166642200170730ustar00rootroot00000000000000import 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 public-4.1.0/src/public/public.py000066400000000000000000000072351460166642200167050ustar00rootroot00000000000000import sys # Tuple can go away in Python >= 3.9 # Union can go away in Python >= 3.10 from typing import Any, Tuple, Union, overload from .types import ModuleAware @overload def public(thing: ModuleAware) -> ModuleAware: ... # pragma: no cover @overload def public(**kws: Any) -> Union[Any, Tuple[Any]]: ... # 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. 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): raise ValueError(f'__all__ must be a list not: {type(dunder_all)}') 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) 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 public-4.1.0/src/public/py.typed000066400000000000000000000000001460166642200165330ustar00rootroot00000000000000public-4.1.0/src/public/types.py000066400000000000000000000001531460166642200165630ustar00rootroot00000000000000from typing import Any, Callable, TypeVar ModuleAware = TypeVar('ModuleAware', bound=Callable[..., Any]) public-4.1.0/test/000077500000000000000000000000001460166642200137605ustar00rootroot00000000000000public-4.1.0/test/__init__.py000066400000000000000000000000001460166642200160570ustar00rootroot00000000000000public-4.1.0/test/test_mypy.py000066400000000000000000000003141460166642200163650ustar00rootroot00000000000000# 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-4.1.0/test/test_private.py000066400000000000000000000016331460166642200170460ustar00rootroot00000000000000from 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') public-4.1.0/test/test_public.py000066400000000000000000000056421460166642200166560ustar00rootroot00000000000000import builtins from 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(ValueError): import_module('example')