| Dan Willemsen | adad21e | 2022-03-25 17:22:05 -0700 | [diff] [blame^] | 1 | .. _Creating ``distutils`` Extensions: |
| 2 | |
| 3 | Creating ``distutils`` Extensions |
| 4 | ================================= |
| 5 | |
| 6 | It can be hard to add new commands or setup arguments to the distutils. But |
| 7 | the ``setuptools`` package makes it a bit easier, by allowing you to distribute |
| 8 | a distutils extension as a separate project, and then have projects that need |
| 9 | the extension just refer to it in their ``setup_requires`` argument. |
| 10 | |
| 11 | With ``setuptools``, your distutils extension projects can hook in new |
| 12 | commands and ``setup()`` arguments just by defining "entry points". These |
| 13 | are mappings from command or argument names to a specification of where to |
| 14 | import a handler from. (See the section on :ref:`Dynamic Discovery of |
| 15 | Services and Plugins` above for some more background on entry points.) |
| 16 | |
| 17 | |
| 18 | Adding Commands |
| 19 | --------------- |
| 20 | |
| 21 | You can add new ``setup`` commands by defining entry points in the |
| 22 | ``distutils.commands`` group. For example, if you wanted to add a ``foo`` |
| 23 | command, you might add something like this to your distutils extension |
| 24 | project's setup script:: |
| 25 | |
| 26 | setup( |
| 27 | # ... |
| 28 | entry_points={ |
| 29 | "distutils.commands": [ |
| 30 | "foo = mypackage.some_module:foo", |
| 31 | ], |
| 32 | }, |
| 33 | ) |
| 34 | |
| 35 | (Assuming, of course, that the ``foo`` class in ``mypackage.some_module`` is |
| 36 | a ``setuptools.Command`` subclass.) |
| 37 | |
| 38 | Once a project containing such entry points has been activated on ``sys.path``, |
| 39 | (e.g. by running "install" or "develop" with a site-packages installation |
| 40 | directory) the command(s) will be available to any ``setuptools``-based setup |
| 41 | scripts. It is not necessary to use the ``--command-packages`` option or |
| 42 | to monkeypatch the ``distutils.command`` package to install your commands; |
| 43 | ``setuptools`` automatically adds a wrapper to the distutils to search for |
| 44 | entry points in the active distributions on ``sys.path``. In fact, this is |
| 45 | how setuptools' own commands are installed: the setuptools project's setup |
| 46 | script defines entry points for them! |
| 47 | |
| 48 | .. note:: |
| 49 | When creating commands, and specially when defining custom ways of building |
| 50 | compiled extensions (for example via ``build_ext``), consider |
| 51 | handling exceptions such as ``CompileError``, ``LinkError``, ``LibError``, |
| 52 | among others. These exceptions are available in the ``setuptools.errors`` |
| 53 | module. |
| 54 | |
| 55 | |
| 56 | Adding ``setup()`` Arguments |
| 57 | ---------------------------- |
| 58 | |
| 59 | .. warning:: Adding arguments to setup is discouraged as such arguments |
| 60 | are only supported through imperative execution and not supported through |
| 61 | declarative config. |
| 62 | |
| 63 | Sometimes, your commands may need additional arguments to the ``setup()`` |
| 64 | call. You can enable this by defining entry points in the |
| 65 | ``distutils.setup_keywords`` group. For example, if you wanted a ``setup()`` |
| 66 | argument called ``bar_baz``, you might add something like this to your |
| 67 | distutils extension project's setup script:: |
| 68 | |
| 69 | setup( |
| 70 | # ... |
| 71 | entry_points={ |
| 72 | "distutils.commands": [ |
| 73 | "foo = mypackage.some_module:foo", |
| 74 | ], |
| 75 | "distutils.setup_keywords": [ |
| 76 | "bar_baz = mypackage.some_module:validate_bar_baz", |
| 77 | ], |
| 78 | }, |
| 79 | ) |
| 80 | |
| 81 | The idea here is that the entry point defines a function that will be called |
| 82 | to validate the ``setup()`` argument, if it's supplied. The ``Distribution`` |
| 83 | object will have the initial value of the attribute set to ``None``, and the |
| 84 | validation function will only be called if the ``setup()`` call sets it to |
| 85 | a non-None value. Here's an example validation function:: |
| 86 | |
| 87 | def assert_bool(dist, attr, value): |
| 88 | """Verify that value is True, False, 0, or 1""" |
| 89 | if bool(value) != value: |
| 90 | raise DistutilsSetupError( |
| 91 | "%r must be a boolean value (got %r)" % (attr,value) |
| 92 | ) |
| 93 | |
| 94 | Your function should accept three arguments: the ``Distribution`` object, |
| 95 | the attribute name, and the attribute value. It should raise a |
| 96 | ``DistutilsSetupError`` (from the ``distutils.errors`` module) if the argument |
| 97 | is invalid. Remember, your function will only be called with non-None values, |
| 98 | and the default value of arguments defined this way is always None. So, your |
| 99 | commands should always be prepared for the possibility that the attribute will |
| 100 | be ``None`` when they access it later. |
| 101 | |
| 102 | If more than one active distribution defines an entry point for the same |
| 103 | ``setup()`` argument, *all* of them will be called. This allows multiple |
| 104 | distutils extensions to define a common argument, as long as they agree on |
| 105 | what values of that argument are valid. |
| 106 | |
| 107 | Also note that as with commands, it is not necessary to subclass or monkeypatch |
| 108 | the distutils ``Distribution`` class in order to add your arguments; it is |
| 109 | sufficient to define the entry points in your extension, as long as any setup |
| 110 | script using your extension lists your project in its ``setup_requires`` |
| 111 | argument. |
| 112 | |
| 113 | |
| 114 | Customizing Distribution Options |
| 115 | -------------------------------- |
| 116 | |
| 117 | Plugins may wish to extend or alter the options on a Distribution object to |
| 118 | suit the purposes of that project. For example, a tool that infers the |
| 119 | ``Distribution.version`` from SCM-metadata may need to hook into the |
| 120 | option finalization. To enable this feature, Setuptools offers an entry |
| 121 | point "setuptools.finalize_distribution_options". That entry point must |
| 122 | be a callable taking one argument (the Distribution instance). |
| 123 | |
| 124 | If the callable has an ``.order`` property, that value will be used to |
| 125 | determine the order in which the hook is called. Lower numbers are called |
| 126 | first and the default is zero (0). |
| 127 | |
| 128 | Plugins may read, alter, and set properties on the distribution, but each |
| 129 | plugin is encouraged to load the configuration/settings for their behavior |
| 130 | independently. |
| 131 | |
| 132 | |
| 133 | .. _Adding new EGG-INFO Files: |
| 134 | |
| 135 | Adding new EGG-INFO Files |
| 136 | ------------------------- |
| 137 | |
| 138 | Some extensible applications or frameworks may want to allow third parties to |
| 139 | develop plugins with application or framework-specific metadata included in |
| 140 | the plugins' EGG-INFO directory, for easy access via the ``pkg_resources`` |
| 141 | metadata API. The easiest way to allow this is to create a distutils extension |
| 142 | to be used from the plugin projects' setup scripts (via ``setup_requires``) |
| 143 | that defines a new setup keyword, and then uses that data to write an EGG-INFO |
| 144 | file when the ``egg_info`` command is run. |
| 145 | |
| 146 | The ``egg_info`` command looks for extension points in an ``egg_info.writers`` |
| 147 | group, and calls them to write the files. Here's a simple example of a |
| 148 | distutils extension defining a setup argument ``foo_bar``, which is a list of |
| 149 | lines that will be written to ``foo_bar.txt`` in the EGG-INFO directory of any |
| 150 | project that uses the argument:: |
| 151 | |
| 152 | setup( |
| 153 | # ... |
| 154 | entry_points={ |
| 155 | "distutils.setup_keywords": [ |
| 156 | "foo_bar = setuptools.dist:assert_string_list", |
| 157 | ], |
| 158 | "egg_info.writers": [ |
| 159 | "foo_bar.txt = setuptools.command.egg_info:write_arg", |
| 160 | ], |
| 161 | }, |
| 162 | ) |
| 163 | |
| 164 | This simple example makes use of two utility functions defined by setuptools |
| 165 | for its own use: a routine to validate that a setup keyword is a sequence of |
| 166 | strings, and another one that looks up a setup argument and writes it to |
| 167 | a file. Here's what the writer utility looks like:: |
| 168 | |
| 169 | def write_arg(cmd, basename, filename): |
| 170 | argname = os.path.splitext(basename)[0] |
| 171 | value = getattr(cmd.distribution, argname, None) |
| 172 | if value is not None: |
| 173 | value = "\n".join(value) + "\n" |
| 174 | cmd.write_or_delete_file(argname, filename, value) |
| 175 | |
| 176 | As you can see, ``egg_info.writers`` entry points must be a function taking |
| 177 | three arguments: a ``egg_info`` command instance, the basename of the file to |
| 178 | write (e.g. ``foo_bar.txt``), and the actual full filename that should be |
| 179 | written to. |
| 180 | |
| 181 | In general, writer functions should honor the command object's ``dry_run`` |
| 182 | setting when writing files, and use the ``distutils.log`` object to do any |
| 183 | console output. The easiest way to conform to this requirement is to use |
| 184 | the ``cmd`` object's ``write_file()``, ``delete_file()``, and |
| 185 | ``write_or_delete_file()`` methods exclusively for your file operations. See |
| 186 | those methods' docstrings for more details. |
| 187 | |
| 188 | |
| 189 | .. _Adding Support for Revision Control Systems: |
| 190 | |
| 191 | Adding Support for Revision Control Systems |
| 192 | ------------------------------------------------- |
| 193 | |
| 194 | If the files you want to include in the source distribution are tracked using |
| 195 | Git, Mercurial or SVN, you can use the following packages to achieve that: |
| 196 | |
| 197 | - Git and Mercurial: :pypi:`setuptools_scm` |
| 198 | - SVN: :pypi:`setuptools_svn` |
| 199 | |
| 200 | If you would like to create a plugin for ``setuptools`` to find files tracked |
| 201 | by another revision control system, you can do so by adding an entry point to |
| 202 | the ``setuptools.file_finders`` group. The entry point should be a function |
| 203 | accepting a single directory name, and should yield all the filenames within |
| 204 | that directory (and any subdirectories thereof) that are under revision |
| 205 | control. |
| 206 | |
| 207 | For example, if you were going to create a plugin for a revision control system |
| 208 | called "foobar", you would write a function something like this: |
| 209 | |
| 210 | .. code-block:: python |
| 211 | |
| 212 | def find_files_for_foobar(dirname): |
| 213 | ... # loop to yield paths that start with `dirname` |
| 214 | |
| 215 | And you would register it in a setup script using something like this:: |
| 216 | |
| 217 | entry_points={ |
| 218 | "setuptools.file_finders": [ |
| 219 | "foobar = my_foobar_module:find_files_for_foobar", |
| 220 | ] |
| 221 | } |
| 222 | |
| 223 | Then, anyone who wants to use your plugin can simply install it, and their |
| 224 | local setuptools installation will be able to find the necessary files. |
| 225 | |
| 226 | It is not necessary to distribute source control plugins with projects that |
| 227 | simply use the other source control system, or to specify the plugins in |
| 228 | ``setup_requires``. When you create a source distribution with the ``sdist`` |
| 229 | command, setuptools automatically records what files were found in the |
| 230 | ``SOURCES.txt`` file. That way, recipients of source distributions don't need |
| 231 | to have revision control at all. However, if someone is working on a package |
| 232 | by checking out with that system, they will need the same plugin(s) that the |
| 233 | original author is using. |
| 234 | |
| 235 | A few important points for writing revision control file finders: |
| 236 | |
| 237 | * Your finder function MUST return relative paths, created by appending to the |
| 238 | passed-in directory name. Absolute paths are NOT allowed, nor are relative |
| 239 | paths that reference a parent directory of the passed-in directory. |
| 240 | |
| 241 | * Your finder function MUST accept an empty string as the directory name, |
| 242 | meaning the current directory. You MUST NOT convert this to a dot; just |
| 243 | yield relative paths. So, yielding a subdirectory named ``some/dir`` under |
| 244 | the current directory should NOT be rendered as ``./some/dir`` or |
| 245 | ``/somewhere/some/dir``, but *always* as simply ``some/dir`` |
| 246 | |
| 247 | * Your finder function SHOULD NOT raise any errors, and SHOULD deal gracefully |
| 248 | with the absence of needed programs (i.e., ones belonging to the revision |
| 249 | control system itself. It *may*, however, use ``distutils.log.warn()`` to |
| 250 | inform the user of the missing program(s). |