Commit 899e59ff authored by PJ Eby's avatar PJ Eby

Allow distutils extensions to define new kinds of metadata that can be

written to EGG-INFO.  Extensible applications and frameworks can thus make
it possible for plugin projects to supply setup() metadata that can then
be used by the application or framework.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041183
parent 568f7f51
......@@ -40,7 +40,6 @@ setup(
scripts = ['easy_install.py'],
zip_safe = False, # We want 'python -m easy_install' to work :(
entry_points = {
"distutils.commands" : [
"%(cmd)s = setuptools.command.%(cmd)s:%(cmd)s" % locals()
......@@ -50,13 +49,23 @@ setup(
"eager_resources = setuptools.dist:assert_string_list",
"namespace_packages = setuptools.dist:check_nsp",
"extras_require = setuptools.dist:check_extras",
"install_requires = setuptools.dist:check_install_requires",
"entry_points = setuptools.dist:check_entry_points",
"test_suite = setuptools.dist:check_test_suite",
"zip_safe = setuptools.dist:assert_bool",
]
],
"egg_info.writers": [
"PKG-INFO = setuptools.command.egg_info:write_pkg_info",
"requires.txt = setuptools.command.egg_info:write_requirements",
"entry_points.txt = setuptools.command.egg_info:write_entries",
"eager_resources.txt = setuptools.command.egg_info:write_arg",
"namespace_packages.txt = setuptools.command.egg_info:write_arg",
"top_level.txt = setuptools.command.egg_info:write_toplevel_names",
"depends.txt = setuptools.command.egg_info:warn_depends_obsolete",
],
},
setup_requires = ['setuptools>=0.6a0'],
# uncomment for testing
# setup_requires = ['setuptools>=0.6a0'],
classifiers = [f.strip() for f in """
Development Status :: 3 - Alpha
......@@ -68,23 +77,6 @@ setup(
Topic :: Software Development :: Libraries :: Python Modules
Topic :: System :: Archiving :: Packaging
Topic :: System :: Systems Administration
Topic :: Utilities
""".splitlines() if f.strip()]
Topic :: Utilities""".splitlines() if f.strip()]
)
[distutils.setup_keywords]
entry_points = setuptools.dist:check_entry_points
extras_require = setuptools.dist:check_extras
install_requires = setuptools.dist:check_install_requires
namespace_packages = setuptools.dist:check_nsp
test_suite = setuptools.dist:check_test_suite
eager_resources = setuptools.dist:assert_string_list
zip_safe = setuptools.dist:assert_bool
[egg_info.writers]
requires.txt = setuptools.command.egg_info:write_requirements
PKG-INFO = setuptools.command.egg_info:write_pkg_info
eager_resources.txt = setuptools.command.egg_info:write_arg
top_level.txt = setuptools.command.egg_info:write_toplevel_names
namespace_packages.txt = setuptools.command.egg_info:write_arg
entry_points.txt = setuptools.command.egg_info:write_entries
depends.txt = setuptools.command.egg_info:warn_depends_obsolete
[distutils.commands]
rotate = setuptools.command.rotate:rotate
develop = setuptools.command.develop:develop
......
......@@ -622,6 +622,21 @@ invoking app or framework can ignore such errors if it wants to make an entry
point optional if a requirement isn't installed.)
Defining Additional Metadata
----------------------------
Some extensible applications and frameworks may need to define their own kinds
of metadata to include in eggs, which they can then access using the
``pkg_resources`` metadata APIs. Ordinarily, this is done by having plugin
developers include additional files in their ``ProjectName.egg-info``
directory. However, since it can be tedious to create such files by hand, you
may want to create a distutils extension that will create the necessary files
from arguments to ``setup()``, in much the same way that ``setuptools`` does
for many of the ``setup()`` arguments it adds. See the section below on
`Creating distutils Extensions`_ for more details, especially the subsection on
`Adding new EGG-INFO Files`_.
"Development Mode"
==================
......@@ -1301,6 +1316,14 @@ the project's source directory or metadata should get it from this setting:
``package_dir`` argument to the ``setup()`` function, if any. If there is
no ``package_dir`` set, this option defaults to the current directory.
In addition to writing the core egg metadata defined by ``setuptools`` and
required by ``pkg_resources``, this command can be extended to write other
metadata files as well, by defining entry points in the ``egg_info.writers``
group. See the section on `Adding new EGG-INFO Files`_ below for more details.
Note that using additional metadata writers may require you to include a
``setup_requires`` argument to ``setup()`` in order to ensure that the desired
writers are available on ``sys.path``.
.. _rotate:
......@@ -1639,6 +1662,60 @@ sufficient to define the entry points in your extension, as long as the setup
script lists your extension in its ``setup_requires`` argument.
Adding new EGG-INFO Files
-------------------------
Some extensible applications or frameworks may want to allow third parties to
develop plugins with application or framework-specific metadata included in
the plugins' EGG-INFO directory, for easy access via the ``pkg_resources``
metadata API. The easiest way to allow this is to create a distutils extension
to be used from the plugin projects' setup scripts (via ``setup_requires``)
that defines a new setup keyword, and then uses that data to write an EGG-INFO
file when the ``egg_info`` command is run.
The ``egg_info`` command looks for extension points in an ``egg_info.writers``
group, and calls them to write the files. Here's a simple example of a
distutils extension defining a setup argument ``foo_bar``, which is a list of
lines that will be written to ``foo_bar.txt`` in the EGG-INFO directory of any
project that uses the argument::
setup(
# ...
entry_points = {
"distutils.setup_keywords": [
"foo_bar = setuptools.dist:assert_string_list",
],
"egg_info.writers": [
"foo_bar.txt = setuptools.command.egg_info:write_arg",
],
},
)
This simple example makes use of two utility functions defined by setuptools
for its own use: a routine to validate that a setup keyword is a sequence of
strings, and another one that looks up a setup argument and writes it to
a file. Here's what the writer utility looks like::
def write_arg(cmd, basename, filename):
argname = os.path.splitext(basename)[0]
value = getattr(cmd.distribution, argname, None)
if value is not None:
value = '\n'.join(value)+'\n'
cmd.write_or_delete_file(argname, filename, value)
As you can see, ``egg_info.writers`` entry points must be a function taking
three arguments: a ``egg_info`` command instance, the basename of the file to
write (e.g. ``foo_bar.txt``), and the actual full filename that should be
written to.
In general, writer functions should honor the command object's ``dry_run``
setting when writing files, and use the ``distutils.log`` object to do any
console output. The easiest way to conform to this requirement is to use
the ``cmd`` object's ``write_file()``, ``delete_file()``, and
``write_or_delete_file()`` methods exclusively for your file operations. See
those methods' docstrings for more details.
Subclassing ``Command``
-----------------------
......@@ -1709,9 +1786,10 @@ Release Notes/Change History
``setup_requires`` allows you to automatically find and download packages
that are needed in order to *build* your project (as opposed to running it).
* ``setuptools`` now finds its commands and ``setup()`` argument validators
using entry points, so that they are extensible by third-party packages.
See `Creating distutils Extensions`_ above for more details.
* ``setuptools`` now finds its commands, ``setup()`` argument validators, and
metadata writers using entry points, so that they can be extended by
third-party packages. See `Creating distutils Extensions`_ above for more
details.
* The vestigial ``depends`` command has been removed. It was never finished
or documented, and never would have worked without EasyInstall - which it
......
......@@ -8,7 +8,7 @@ from setuptools import Command
from distutils.errors import *
from distutils import log
from pkg_resources import parse_requirements, safe_name, \
safe_version, yield_lines, EntryPoint
safe_version, yield_lines, EntryPoint, iter_entry_points
class egg_info(Command):
......@@ -80,47 +80,55 @@ class egg_info(Command):
def run(self):
# Make the .egg-info directory, then write PKG-INFO and requires.txt
self.mkpath(self.egg_info)
log.info("writing %s" % os.path.join(self.egg_info,'PKG-INFO'))
if not self.dry_run:
metadata = self.distribution.metadata
metadata.version, oldver = self.egg_version, metadata.version
metadata.name, oldname = self.egg_name, metadata.name
try:
# write unescaped data to PKG-INFO, so older pkg_resources
# can still parse it
metadata.write_pkg_info(self.egg_info)
finally:
metadata.name, metadata.version = oldname, oldver
self.write_entry_points()
self.write_requirements()
self.write_toplevel_names()
self.write_or_delete_dist_arg('namespace_packages')
self.write_or_delete_dist_arg('eager_resources')
if os.path.exists(os.path.join(self.egg_info,'depends.txt')):
log.warn(
"WARNING: 'depends.txt' is not used by setuptools 0.6!\n"
"Use the install_requires/extras_require setup() args instead."
)
def write_or_delete_file(self, what, filename, data):
"""Write `data` to `filename` or delete if empty
def write_requirements(self):
dist = self.distribution
if not getattr(dist,'install_requires',None) and \
not getattr(dist,'extras_require',None): return
If `data` is non-empty, this routine is the same as ``write_file()``.
If `data` is empty but not ``None``, this is the same as calling
``delete_file(filename)`. If `data` is ``None``, then this is a no-op
unless `filename` exists, in which case a warning is issued about the
orphaned file.
"""
if data:
self.write_file(what, filename, data)
elif os.path.exists(filename):
if data is None:
log.warn(
"%s not set in setup(), but %s exists", what, filename
)
return
else:
self.delete_file(filename)
requires = os.path.join(self.egg_info,"requires.txt")
log.info("writing %s", requires)
def write_file(self, what, filename, data):
"""Write `data` to `filename` (if not a dry run) after announcing it
`what` is used in a log message to identify what is being written
to the file.
"""
log.info("writing %s to %s", what, filename)
if not self.dry_run:
f = open(requires, 'wt')
f.write('\n'.join(yield_lines(dist.install_requires)))
for extra,reqs in dist.extras_require.items():
f.write('\n\n[%s]\n%s' % (extra, '\n'.join(yield_lines(reqs))))
f = open(filename, 'wb')
f.write(data)
f.close()
def delete_file(self, filename):
"""Delete `filename` (if not a dry run) after announcing it"""
log.info("deleting %s", filename)
if not self.dry_run:
os.unlink(filename)
def run(self):
# Make the .egg-info directory, then write PKG-INFO and requires.txt
self.mkpath(self.egg_info)
installer = self.distribution.fetch_build_egg
for ep in iter_entry_points('egg_info.writers'):
writer = ep.load(installer=installer)
writer(self, ep.name, os.path.join(self.egg_info,ep.name))
def tagged_version(self):
version = self.distribution.get_version()
if self.tag_build:
......@@ -132,7 +140,6 @@ class egg_info(Command):
version += time.strftime("-%Y%m%d")
return safe_version(version)
def get_svn_revision(self):
stdin, stdout = os.popen4("svn info -R"); stdin.close()
result = stdout.read(); stdout.close()
......@@ -146,60 +153,94 @@ class egg_info(Command):
return str(max(revisions))
def write_toplevel_names(self):
pkgs = dict.fromkeys(
[k.split('.',1)[0]
for k in self.distribution.iter_distribution_names()
]
def write_pkg_info(cmd, basename, filename):
log.info("writing %s", filename)
if not cmd.dry_run:
metadata = cmd.distribution.metadata
metadata.version, oldver = cmd.egg_version, metadata.version
metadata.name, oldname = cmd.egg_name, metadata.name
try:
# write unescaped data to PKG-INFO, so older pkg_resources
# can still parse it
metadata.write_pkg_info(cmd.egg_info)
finally:
metadata.name, metadata.version = oldname, oldver
def warn_depends_obsolete(cmd, basename, filename):
if os.path.exists(filename):
log.warn(
"WARNING: 'depends.txt' is not used by setuptools 0.6!\n"
"Use the install_requires/extras_require setup() args instead."
)
toplevel = os.path.join(self.egg_info, "top_level.txt")
log.info("writing list of top-level names to %s" % toplevel)
if not self.dry_run:
f = open(toplevel, 'wt')
f.write('\n'.join(pkgs))
f.write('\n')
f.close()
def write_requirements(cmd, basename, filename):
dist = cmd.distribution
data = ['\n'.join(yield_lines(dist.install_requires or ()))]
for extra,reqs in (dist.extras_require or {}).items():
data.append('\n\n[%s]\n%s' % (extra, '\n'.join(yield_lines(reqs))))
cmd.write_or_delete_file("requirements", filename, ''.join(data))
def write_toplevel_names(cmd, basename, filename):
pkgs = dict.fromkeys(
[k.split('.',1)[0]
for k in cmd.distribution.iter_distribution_names()
]
)
cmd.write_file("top-level names", filename, '\n'.join(pkgs)+'\n')
def write_arg(cmd, basename, filename):
argname = os.path.splitext(basename)[0]
value = getattr(cmd.distribution, argname, None)
if value is not None:
value = '\n'.join(value)+'\n'
cmd.write_or_delete_file(argname, filename, value)
def write_entries(cmd, basename, filename):
ep = cmd.distribution.entry_points
if isinstance(ep,basestring) or ep is None:
data = ep
elif ep is not None:
data = []
for section, contents in ep.items():
if not isinstance(contents,basestring):
contents = EntryPoint.parse_list(section, contents)
contents = '\n'.join(map(str,contents.values()))
data.append('[%s]\n%s\n\n' % (section,contents))
data = ''.join(data)
cmd.write_or_delete_file('entry points', filename, data)
def write_or_delete_dist_arg(self, argname, filename=None):
value = getattr(self.distribution, argname, None)
filename = filename or argname+'.txt'
filename = os.path.join(self.egg_info,filename)
if value:
log.info("writing %s", filename)
if not self.dry_run:
f = open(filename, 'wt')
f.write('\n'.join(value))
f.write('\n')
f.close()
elif os.path.exists(filename):
if value is None:
log.warn(
"%s not set in setup(), but %s exists", argname, filename
)
return
log.info("deleting %s", filename)
if not self.dry_run:
os.unlink(filename)
def write_entry_points(self):
ep = getattr(self.distribution,'entry_points',None)
if ep is None:
return
epname = os.path.join(self.egg_info,"entry_points.txt")
log.info("writing %s", epname)
if not self.dry_run:
f = open(epname, 'wt')
if isinstance(ep,basestring):
f.write(ep)
else:
for section, contents in ep.items():
if not isinstance(contents,basestring):
contents = EntryPoint.parse_list(section, contents)
contents = '\n'.join(map(str,contents.values()))
f.write('[%s]\n%s\n\n' % (section,contents))
f.close()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment