From 42016db0ad2ce01d9fff5f070eae4fbde842cb1e Mon Sep 17 00:00:00 2001 From: JinWang An Date: Mon, 27 Mar 2023 17:02:50 +0900 Subject: [PATCH] Imported Upstream version 62.3.4 --- .bumpversion.cfg | 2 +- CHANGES.rst | 15 + docs/conf.py | 2 +- docs/references/keywords.rst | 41 ++- docs/userguide/datafiles.rst | 9 +- docs/userguide/declarative_config.rst | 37 ++- docs/userguide/entry_point.rst | 455 ++++++++++++++++++++++---- setup.cfg | 2 +- setuptools/command/build_py.py | 7 +- 9 files changed, 496 insertions(+), 74 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 987e30e..fb32783 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 62.3.3 +current_version = 62.3.4 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 4d9c973..70c9897 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,18 @@ +v62.3.4 +------- + + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #3349: Fixed two small issues preventing docs from building locally -- by :user:`codeandfire` +* #3350: Added note explaining ``package_data`` glob pattern matching for dotfiles -- by :user:`comabrewer` +* #3358: Clarify the role of the ``package_dir`` configuration. + +Misc +^^^^ +* #3354: Improve clarity in warning about unlisted namespace packages. + + v62.3.3 ------- diff --git a/docs/conf.py b/docs/conf.py index 1023539..9028691 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,7 +103,7 @@ github_repo_slug = f'{github_repo_org}/{github_repo_name}' github_repo_url = f'{github_url}/{github_repo_slug}' github_sponsors_url = f'{github_url}/sponsors' extlinks = { - 'user': (f'{github_sponsors_url}/%s', '@'), # noqa: WPS323 + 'user': (f'{github_sponsors_url}/%s', '@%s'), # noqa: WPS323 'pypi': ('https://pypi.org/project/%s', '%s'), # noqa: WPS323 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), # noqa: WPS323 } diff --git a/docs/references/keywords.rst b/docs/references/keywords.rst index d366300..a66d503 100644 --- a/docs/references/keywords.rst +++ b/docs/references/keywords.rst @@ -194,7 +194,46 @@ extensions). .. _keyword/package_dir: ``package_dir`` - A dictionary providing a mapping of package to directory names. + A dictionary that maps package names (as they will be + imported by the end-users) into directory paths (that actually exist in the + project's source tree). This configuration has two main purposes: + + 1. To effectively "rename" paths when building your package. + For example, ``package_dir={"mypkg": "dir1/dir2/code_for_mypkg"}`` + will instruct setuptools to copy the ``dir1/dir2/code_for_mypkg/...`` files + as ``mypkg/...`` when building the final :term:`wheel distribution `. + + .. attention:: + While it is *possible* to specify arbitrary mappings, developers are + **STRONGLY ADVISED AGAINST** that. They should try as much as possible + to keep the directory names and hierarchy identical to the way they will + appear in the final wheel, only deviating when absolutely necessary. + + 2. To indicate that the relevant code is entirely contained inside + a specific directory (instead of directly placed under the project's root). + In this case, a special key is required (the empty string, ``""``), + for example: ``package_dir={"": ""}``. + All the directories inside the container directory will be copied + directly into the final :term:`wheel distribution `, but the + container directory itself will not. + + This practice is very common in the community to help separate the + package implementation from auxiliary files (e.g. CI configuration files), + and is referred to as :ref:`src-layout`, because the container + directory is commonly named ``src``. + + All paths in ``package_dir`` must be relative to the project root directory + and use a forward slash (``/``) as path separator regardless of the + operating system. + + .. tip:: + When using :doc:`package discovery ` + together with :doc:`setup.cfg ` or + :doc:`pyproject.toml `, it is very likely + that you don't need to specify a value for ``package_dir``. Please have + a look at the definitions of :ref:`src-layout` and :ref:`flat-layout` to + learn common practices on how to design a project's directory structure + and minimise the amount of configuration that is needed. .. _keyword/requires: diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index 8622b6c..3a2ffbd 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -157,6 +157,11 @@ require to be added by a revision control system plugin. the path separator, even if you are on Windows. Setuptools automatically converts slashes to appropriate platform-specific separators at build time. +.. note:: + Glob patterns do not automatically match dotfiles (directory or file names + starting with a dot (``.``)). To include such files, you must explicitly start + the pattern with a dot, e.g. ``.*`` to match ``.gitignore``. + If you have multiple top-level packages and a common pattern of data files for all these packages, for example:: @@ -243,8 +248,8 @@ Sometimes, the ``include_package_data`` or ``package_data`` options alone aren't sufficient to precisely define what files you want included. For example, consider a scenario where you have ``include_package_data=True``, and you are using a revision control system with an appropriate plugin. -Sometimes developers add directory-specific marker files (such as `.gitignore`, -`.gitkeep`, `.gitattributes`, or `.hgignore`), these files are probably being +Sometimes developers add directory-specific marker files (such as ``.gitignore``, +``.gitkeep``, ``.gitattributes``, or ``.hgignore``), these files are probably being tracked by the revision control system, and therefore by default they will be included when the package is installed. diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst index 2a65e6e..aa8bc7e 100644 --- a/docs/userguide/declarative_config.rst +++ b/docs/userguide/declarative_config.rst @@ -62,8 +62,8 @@ boilerplate code in some cases. Metadata and options are set in the config sections of the same name. -* Keys are the same as the keyword arguments one provides to the ``setup()`` - function. +* Keys are the same as the :doc:`keyword arguments ` one + provides to the ``setup()`` function. * Complex values can be written comma-separated or placed one per line in *dangling* config values. The following are equivalent: @@ -90,7 +90,7 @@ Metadata and options are set in the config sections of the same name. Using a ``src/`` layout ======================= -One commonly used package configuration has all the module source code in a +One commonly used configuration has all the Python source code in a subdirectory (often called the ``src/`` layout), like this:: ├── src @@ -101,7 +101,7 @@ subdirectory (often called the ``src/`` layout), like this:: └── setup.cfg You can set up your ``setup.cfg`` to automatically find all your packages in -the subdirectory like this: +the subdirectory, using :ref:`package_dir `, like this: .. code-block:: ini @@ -116,6 +116,22 @@ the subdirectory like this: [options.packages.find] where=src +In this example, the value for the :ref:`package_dir ` +configuration (i.e. ``=src``) is parsed as ``{"": "src"}``. +The ``""`` key has a special meaning in this context, and indicates that all the +packages are contained inside the given directory. +Also note that the value for ``[options.packages.find] where`` matches the +value associated with ``""`` in the ``package_dir`` dictionary. + +.. + TODO: Add the following tip once the auto-discovery is no longer experimental: + + Starting in version 61, ``setuptools`` can automatically infer the + configurations for both ``packages`` and ``package_dir`` for projects using + a ``src/`` layout (as long as no value is specified for ``py_modules``). + Please see :doc:`package discovery ` for more + details. + Specifying values ================= @@ -127,7 +143,10 @@ Type names used below: * ``list-comma`` - dangling list or string of comma-separated values * ``list-semi`` - dangling list or string of semicolon-separated values * ``bool`` - ``True`` is 1, yes, true -* ``dict`` - list-comma where keys are separated from values by ``=`` +* ``dict`` - list-comma where each entry corresponds to a key/value pair, + with keys separated from values by ``=``. + If an entry starts with ``=``, the key is assumed to be an empty string + (e.g. ``=src`` is parsed as ``{"": "src"}``). * ``section`` - values are read from a dedicated (sub)section @@ -143,15 +162,15 @@ Special directives: * ``file:`` - Value is read from a list of files and then concatenated - .. note:: - The ``file:`` directive is sandboxed and won't reach anything outside - the directory containing ``setup.py``. + .. important:: + The ``file:`` directive is sandboxed and won't reach anything outside the + project directory (i.e. the directory containing ``setup.cfg``/``pyproject.toml``). Metadata -------- -.. note:: +.. attention:: The aliases given below are supported for compatibility reasons, but their use is not advised. diff --git a/docs/userguide/entry_point.rst b/docs/userguide/entry_point.rst index b97419c..9dca389 100644 --- a/docs/userguide/entry_point.rst +++ b/docs/userguide/entry_point.rst @@ -14,15 +14,14 @@ Console Scripts =============== First consider an example without entry points. Imagine a package -defined thus: +defined thus:: -.. code-block:: bash - - timmins/ - timmins/__init__.py - timmins/__main__.py - setup.cfg # or setup.py - #other necessary files + project_root_directory + ├── setup.py # and/or setup.cfg, pyproject.toml + └── src + └── timmins + ├── __init__.py + └── ... with ``__init__.py`` as: @@ -31,7 +30,10 @@ with ``__init__.py`` as: def hello_world(): print("Hello world") -and ``__main__.py`` providing a hook: +Now, suppose that we would like to provide some way of executing the +function ``hello_world()`` from the command-line. One way to do this +is to create a file ``src/timmins/__main__.py`` providing a hook as +follows: .. code-block:: python @@ -40,19 +42,20 @@ and ``__main__.py`` providing a hook: if __name__ == '__main__': hello_world() -After installing the package, the function may be invoked through the -`runpy `_ module: +Then, after installing the package ``timmins``, we may invoke the ``hello_world()`` +function as follows, through the `runpy `_ +module: .. code-block:: bash - python -m timmins + $ python -m timmins + Hello world -Adding a console script entry point allows the package to define a -user-friendly name for installers of the package to execute. Installers -like pip will create wrapper scripts to execute a function. In the -above example, to create a command ``hello-world`` that invokes -``timmins.hello_world``, add a console script entry point to -``setup.cfg``: +Instead of this approach using ``__main__.py``, you can also create a +user-friendly CLI executable that can be called directly without ``python -m``. +In the above example, to create a command ``hello-world`` that invokes +``timmins.hello_world``, add a console script entry point to your +configuration: .. tab:: setup.cfg @@ -69,20 +72,35 @@ above example, to create a command ``hello-world`` that invokes from setuptools import setup setup( - name='timmins', - version='0.0.1', - packages=['timmins'], - # ... + # ..., entry_points={ - 'console_scripts': [ - 'hello-world=timmins:hello_world', - ] - } + 'console_scripts': [ + 'hello-world=timmins:hello_world', + ] + } ) +.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_ + + .. code-block:: toml + + [project.scripts] + hello-world = "timmins:hello_world" + After installing the package, a user may invoke that function by simply calling -``hello-world`` on the command line. +``hello-world`` on the command line: + +.. code-block:: bash + + $ hello-world + Hello world + +Note that any function configured as a console script, i.e. ``hello_world()`` in +this example, should not accept any arguments. If your function requires any input +from the user, you can use regular command-line argument parsing utilities like +`argparse `_ within the body of +the function to parse user input given via :obj:`sys.argv`. The syntax for entry points is specified as follows: @@ -94,16 +112,99 @@ where ``name`` is the name for the script you want to create, the left hand side of ``:`` is the module that contains your function and the right hand side is the object you want to invoke (e.g. a function). +GUI Scripts +=========== + In addition to ``console_scripts``, Setuptools supports ``gui_scripts``, which will launch a GUI application without running in a terminal window. +For example, if we have a project with the same directory structure as before, +with an ``__init__.py`` file containing the following: + +.. code-block:: python + + import PySimpleGUI as sg + + def hello_world(): + sg.Window(title="Hello world", layout=[[]], margins=(100, 50)).read() + +Then, we can add a GUI script entry point: + +.. tab:: setup.cfg + + .. code-block:: ini + + [options.entry_points] + gui_scripts = + hello-world = timmins:hello_world + +.. tab:: setup.py + + .. code-block:: python + + from setuptools import setup + + setup( + # ..., + entry_points={ + 'gui_scripts': [ + 'hello-world=timmins:hello_world', + ] + } + ) + +.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_ + + .. code-block:: toml + + [project.gui-scripts] + hello-world = "timmins:hello_world" + +.. note:: + To be able to import ``PySimpleGUI``, you need to add ``pysimplegui`` to your package dependencies. + See :doc:`/userguide/dependency_management` for more information. + +Now, running: + +.. code-block:: bash + + $ hello-world + +will open a small application window with the title 'Hello world'. + +Note that just as with console scripts, any function configured as a GUI script +should not accept any arguments, and any user input can be parsed within the +body of the function. + +.. note:: + + The difference between ``console_scripts`` and ``gui_scripts`` only affects + Windows systems. [#use_for_scripts]_ ``console_scripts`` are wrapped in a console + executable, so they are attached to a console and can use ``sys.stdin``, + ``sys.stdout`` and ``sys.stderr`` for input and output. ``gui_scripts`` are + wrapped in a GUI executable, so they can be started without a console, but + cannot use standard streams unless application code redirects them. Other + platforms do not have the same distinction. + +.. note:: + + Console and GUI scripts work because behind the scenes, installers like :pypi:`pip` + create wrapper scripts around the function(s) being invoked. For example, + the ``hello-world`` entry point in the above two examples would create a + command ``hello-world`` launching a script like this: [#use_for_scripts]_ + + .. code-block:: python + + import sys + from timmins import hello_world + sys.exit(hello_world()) .. _dynamic discovery of services and plugins: Advertising Behavior ==================== -Console scripts are one use of the more general concept of entry points. Entry +Console/GUI scripts are one use of the more general concept of entry points. Entry points more generally allow a packager to advertise behavior for discovery by other libraries and applications. This feature enables "plug-in"-like functionality, where one library solicits entry points and any number of other @@ -114,48 +215,277 @@ A good example of this plug-in behavior can be seen in where pytest is a test framework that allows other libraries to extend or modify its functionality through the ``pytest11`` entry point. -The console scripts work similarly, where libraries advertise their commands +The console/GUI scripts work similarly, where libraries advertise their commands and tools like ``pip`` create wrapper scripts that invoke those commands. -For a project wishing to solicit entry points, Setuptools recommends the -`importlib.metadata `_ -module (part of stdlib since Python 3.8) or its backport, -:pypi:`importlib_metadata`. +Entry Points for Plugins +======================== -For example, to find the console script entry points from the example above: +Let us consider a simple example to understand how we can implement entry points +corresponding to plugins. Say we have a package ``timmins`` with the following +directory structure:: -.. code-block:: pycon + timmins + ├── setup.py # and/or setup.cfg, pyproject.toml + └── src + └── timmins + └── __init__.py - >>> from importlib import metadata - >>> eps = metadata.entry_points()['console_scripts'] +and in ``src/timmins/__init__.py`` we have the following code: -``eps`` is now a list of ``EntryPoint`` objects, one of which corresponds -to the ``hello-world = timmins:hello_world`` defined above. Each ``EntryPoint`` -contains the ``name``, ``group``, and ``value``. It also supplies a ``.load()`` -method to import and load that entry point (module or object). +.. code-block:: python -.. code-block:: ini + def hello_world(): + print('Hello world') - [options.entry_points] - my.plugins = - hello-world = timmins:hello_world +Basically, we have defined a ``hello_world()`` function which will print the text +'Hello world'. Now, let us say we want to print the text 'Hello world' in different +ways. The current function just prints the text as it is - let us say we want another +style in which the text is enclosed within exclamation marks:: + + !!! Hello world !!! + +Let us see how this can be done using plugins. First, let us separate the style of +printing the text from the text itself. In other words, we can change the code in +``src/timmins/__init__.py`` to something like this: + +.. code-block:: python + + def display(text): + print(text) + + def hello_world(): + display('Hello world') + +Here, the ``display()`` function controls the style of printing the text, and the +``hello_world()`` function calls the ``display()`` function to print the text 'Hello +world`. + +Right now the ``display()`` function just prints the text as it is. In order to be able +to customize it, we can do the following. Let us introduce a new *group* of entry points +named ``timmins.display``, and expect plugin packages implementing this entry point +to supply a ``display()``-like function. Next, to be able to automatically discover plugin +packages that implement this entry point, we can use the +:mod:`importlib.metadata` module, +as follows: + +.. code-block:: python + + from importlib.metadata import entry_points + display_eps = entry_points(group='timmins.display') + +.. note:: + Each ``importlib.metadata.EntryPoint`` object is an object containing a ``name``, a + ``group``, and a ``value``. For example, after setting up the plugin package as + described below, ``display_eps`` in the above code will look like this: [#package_metadata]_ -Then, a different project wishing to load 'my.plugins' plugins could run -the following routine to load (and invoke) such plugins: + .. code-block:: python + + ( + EntryPoint(name='excl', value='timmins_plugin_fancy:excl_display', group='timmins.display'), + ..., + ) + +``display_eps`` will now be a list of ``EntryPoint`` objects, each referring to ``display()``-like +functions defined by one or more installed plugin packages. Then, to import a specific +``display()``-like function - let us choose the one corresponding to the first discovered +entry point - we can use the ``load()`` method as follows: + +.. code-block:: python + + display = display_eps[0].load() + +Finally, a sensible behaviour would be that if we cannot find any plugin packages customizing +the ``display()`` function, we should fall back to our default implementation which prints +the text as it is. With this behaviour included, the code in ``src/timmins/__init__.py`` +finally becomes: + +.. code-block:: python + + from importlib.metadata import entry_points + display_eps = entry_points(group='timmins.display') + try: + display = display_eps[0].load() + except IndexError: + def display(text): + print(text) + + def hello_world(): + display('Hello world') + +That finishes the setup on ``timmins``'s side. Next, we need to implement a plugin +which implements the entry point ``timmins.display``. Let us name this plugin +``timmins-plugin-fancy``, and set it up with the following directory structure:: + + timmins-plugin-fancy + ├── setup.py # and/or setup.cfg, pyproject.toml + └── src + └── timmins_plugin_fancy + └── __init__.py + +And then, inside ``src/timmins_plugin_fancy/__init__.py``, we can put a function +named ``excl_display()`` that prints the given text surrounded by exclamation marks: + +.. code-block:: python + + def excl_display(text): + print('!!!', text, '!!!') + +This is the ``display()``-like function that we are looking to supply to the +``timmins`` package. We can do that by adding the following in the configuration +of ``timmins-plugin-fancy``: + +.. tab:: setup.cfg + + .. code-block:: ini + + [options.entry_points] + timmins.display = + excl = timmins_plugin_fancy:excl_display + +.. tab:: setup.py + + .. code-block:: python + + from setuptools import setup + + setup( + # ..., + entry_points = { + 'timmins.display' = [ + 'excl=timmins_plugin_fancy:excl_display' + ] + } + ) + +.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_ + + .. code-block:: toml + + [project.entry-points."timmins.display"] + excl = "timmins_plugin_fancy:excl_display" + +Basically, this configuration states that we are a supplying an entry point +under the group ``timmins.display``. The entry point is named ``excl`` and it +refers to the function ``excl_display`` defined by the package ``timmins_plugin_fancy``. + +Now, if we install both ``timmins`` and ``timmins_plugin_fancy``, we should get +the following: .. code-block:: pycon - >>> from importlib import metadata - >>> eps = metadata.entry_points()['my.plugins'] - >>> for ep in eps: - ... plugin = ep.load() - ... plugin() - ... + >>> from timmins import hello_world + >>> hello_world() + !!! Hello world !!! + +whereas if we only install ``timmins`` and not ``timmins_plugin_fancy``, we should +get the following: + +.. code-block:: pycon + + >>> from timmins import hello_world + >>> hello_world() + Hello world + +Therefore, our plugin works. + +Our plugin could have also defined multiple entry points under the group ``timmins.display``. +For example, in ``src/timmins_plugin_fancy/__init__.py`` we could have two ``display()``-like +functions, as follows: + +.. code-block:: python + + def excl_display(text): + print('!!!', text, '!!!') -The project soliciting the entry points needs not to have any dependency -or prior knowledge about the libraries implementing the entry points, and + def lined_display(text): + print(''.join(['-' for _ in text])) + print(text) + print(''.join(['-' for _ in text])) + +The configuration of ``timmins-plugin-fancy`` would then change to: + +.. tab:: setup.cfg + + .. code-block:: ini + + [options.entry_points] + timmins.display = + excl = timmins_plugin_fancy:excl_display + lined = timmins_plugin_fancy:lined_display + +.. tab:: setup.py + + .. code-block:: python + + from setuptools import setup + + setup( + # ..., + entry_points = { + 'timmins.display' = [ + 'excl=timmins_plugin_fancy:excl_display', + 'lined=timmins_plugin_fancy:lined_display', + ] + } + ) + +.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_ + + .. code-block:: toml + + [project.entry-points."timmins.display"] + excl = "timmins_plugin_fancy:excl_display" + lined = "timmins_plugin_fancy:lined_display" + +On the ``timmins`` side, we can also use a different strategy of loading entry +points. For example, we can search for a specific display style: + +.. code-block:: python + + display_eps = entry_points(group='timmins.display') + try: + display = display_eps['lined'].load() + except KeyError: + # if the 'lined' display is not available, use something else + ... + +Or we can also load all plugins under the given group. Though this might not +be of much use in our current example, there are several scenarios in which this +is useful: + +.. code-block:: python + + display_eps = entry_points(group='timmins.display') + for ep in display_eps: + display = ep.load() + # do something with display + ... + +importlib.metadata +------------------ + +The recommended approach for loading and importing entry points is the +:mod:`importlib.metadata` module, +which is a part of the standard library since Python 3.8. For older versions of +Python, its backport :pypi:`importlib_metadata` should be used. While using the +backport, the only change that has to be made is to replace ``importlib.metadata`` +with ``importlib_metadata``, i.e. + +.. code-block:: python + + from importlib_metadata import entry_points + ... + +Summary +------- + +In summary, entry points allow a package to open its functionalities for +customization via plugins. +The package soliciting the entry points need not have any dependency +or prior knowledge about the plugins implementing the entry points, and downstream users are able to compose functionality by pulling together -libraries implementing the entry points. +plugins implementing the entry points. Dependency Management @@ -179,3 +509,16 @@ In this case, the ``hello-world`` script is only viable if the ``pretty-printer` extra is indicated, and so a plugin host might exclude that entry point (i.e. not install a console script) if the relevant extra dependencies are not installed. + +---- + +.. [#experimental] + Support for specifying package metadata and build configuration options via + ``pyproject.toml`` is experimental and might change + in the future. See :doc:`/userguide/pyproject_config`. + +.. [#use_for_scripts] + Reference: https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts + +.. [#package_metadata] + Reference: https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata diff --git a/setup.cfg b/setup.cfg index c255135..e1037d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 62.3.3 +version = 62.3.4 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py index 86847f0..2fced3d 100644 --- a/setuptools/command/build_py.py +++ b/setuptools/command/build_py.py @@ -263,9 +263,10 @@ class _IncludePackageDataAbuse: ############################ Python recognizes {importable!r} as an importable package, but it is not listed in the `packages` configuration of setuptools. - Currently {importable!r} is only added to the distribution because it may - contain data files, but this behavior is likely to change in future - versions of setuptools (and therefore is considered deprecated). + + {importable!r} has been automatically added to the distribution only + because it may contain data files, but this behavior is likely to change + in future versions of setuptools (and therefore is considered deprecated). Please make sure that {importable!r} is included as a package by using the `packages` configuration field or the proper discovery methods -- 2.34.1