Skip to content

Commit e4460fa

Browse files
committed
Backout b17e9a0ea116 and 50725de303ef, restoring Feature model. Fixes #161 and re-opens #65.
--HG-- extra : amend_source : f14bc0bf6c9f04e16d30ce0abf7bcb944f41ebea
1 parent 3e99a57 commit e4460fa

File tree

3 files changed

+342
-5
lines changed

3 files changed

+342
-5
lines changed

setuptools/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99

1010
import setuptools.version
1111
from setuptools.extension import Extension
12-
from setuptools.dist import Distribution, _get_unpatched
12+
from setuptools.dist import Distribution, Feature, _get_unpatched
1313
from setuptools.depends import Require
1414

1515
__all__ = [
16-
'setup', 'Distribution', 'Command', 'Extension', 'Require',
16+
'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require',
1717
'find_packages'
1818
]
1919

setuptools/dist.py

Lines changed: 256 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import re
44
import os
55
import sys
6+
import warnings
67
import distutils.log
78
import distutils.core
89
import distutils.cmd
910
from distutils.core import Distribution as _Distribution
10-
from distutils.errors import DistutilsSetupError
11+
from distutils.errors import (DistutilsOptionError, DistutilsPlatformError,
12+
DistutilsSetupError)
1113

14+
from setuptools.depends import Require
1215
from setuptools.compat import numeric_types, basestring
1316
import pkg_resources
1417

@@ -134,7 +137,7 @@ def check_packages(dist, attr, value):
134137

135138

136139
class Distribution(_Distribution):
137-
"""Distribution with support for tests and package data
140+
"""Distribution with support for features, tests, and package data
138141
139142
This is an enhanced version of 'distutils.dist.Distribution' that
140143
effectively adds the following new optional keyword arguments to 'setup()':
@@ -161,6 +164,21 @@ class Distribution(_Distribution):
161164
EasyInstall and requests one of your extras, the corresponding
162165
additional requirements will be installed if needed.
163166
167+
'features' **deprecated** -- a dictionary mapping option names to
168+
'setuptools.Feature'
169+
objects. Features are a portion of the distribution that can be
170+
included or excluded based on user options, inter-feature dependencies,
171+
and availability on the current system. Excluded features are omitted
172+
from all setup commands, including source and binary distributions, so
173+
you can create multiple distributions from the same source tree.
174+
Feature names should be valid Python identifiers, except that they may
175+
contain the '-' (minus) sign. Features can be included or excluded
176+
via the command line options '--with-X' and '--without-X', where 'X' is
177+
the name of the feature. Whether a feature is included by default, and
178+
whether you are allowed to control this from the command line, is
179+
determined by the Feature object. See the 'Feature' class for more
180+
information.
181+
164182
'test_suite' -- the name of a test suite to run for the 'test' command.
165183
If the user runs 'python setup.py test', the package will be installed,
166184
and the named test suite will be run. The format is the same as
@@ -182,7 +200,8 @@ class Distribution(_Distribution):
182200
for manipulating the distribution's contents. For example, the 'include()'
183201
and 'exclude()' methods can be thought of as in-place add and subtract
184202
commands that add or remove packages, modules, extensions, and so on from
185-
the distribution.
203+
the distribution. They are used by the feature subsystem to configure the
204+
distribution for the included and excluded features.
186205
"""
187206

188207
_patched_dist = None
@@ -204,6 +223,11 @@ def __init__(self, attrs=None):
204223
have_package_data = hasattr(self, "package_data")
205224
if not have_package_data:
206225
self.package_data = {}
226+
_attrs_dict = attrs or {}
227+
if 'features' in _attrs_dict or 'require_features' in _attrs_dict:
228+
Feature.warn_deprecated()
229+
self.require_features = []
230+
self.features = {}
207231
self.dist_files = []
208232
self.src_root = attrs and attrs.pop("src_root", None)
209233
self.patch_missing_pkg_info(attrs)
@@ -221,6 +245,17 @@ def __init__(self, attrs=None):
221245
# Some people apparently take "version number" too literally :)
222246
self.metadata.version = str(self.metadata.version)
223247

248+
def parse_command_line(self):
249+
"""Process features after parsing command line options"""
250+
result = _Distribution.parse_command_line(self)
251+
if self.features:
252+
self._finalize_features()
253+
return result
254+
255+
def _feature_attrname(self,name):
256+
"""Convert feature name to corresponding option attribute name"""
257+
return 'with_'+name.replace('-','_')
258+
224259
def fetch_build_eggs(self, requires):
225260
"""Resolve pre-setup requirements"""
226261
from pkg_resources import working_set, parse_requirements
@@ -232,6 +267,8 @@ def fetch_build_eggs(self, requires):
232267

233268
def finalize_options(self):
234269
_Distribution.finalize_options(self)
270+
if self.features:
271+
self._set_global_opts_from_features()
235272

236273
for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
237274
value = getattr(self,ep.name,None)
@@ -276,6 +313,47 @@ def fetch_build_egg(self, req):
276313
self._egg_fetcher = cmd
277314
return cmd.easy_install(req)
278315

316+
def _set_global_opts_from_features(self):
317+
"""Add --with-X/--without-X options based on optional features"""
318+
319+
go = []
320+
no = self.negative_opt.copy()
321+
322+
for name,feature in self.features.items():
323+
self._set_feature(name,None)
324+
feature.validate(self)
325+
326+
if feature.optional:
327+
descr = feature.description
328+
incdef = ' (default)'
329+
excdef=''
330+
if not feature.include_by_default():
331+
excdef, incdef = incdef, excdef
332+
333+
go.append(('with-'+name, None, 'include '+descr+incdef))
334+
go.append(('without-'+name, None, 'exclude '+descr+excdef))
335+
no['without-'+name] = 'with-'+name
336+
337+
self.global_options = self.feature_options = go + self.global_options
338+
self.negative_opt = self.feature_negopt = no
339+
340+
def _finalize_features(self):
341+
"""Add/remove features and resolve dependencies between them"""
342+
343+
# First, flag all the enabled items (and thus their dependencies)
344+
for name,feature in self.features.items():
345+
enabled = self.feature_is_included(name)
346+
if enabled or (enabled is None and feature.include_by_default()):
347+
feature.include_in(self)
348+
self._set_feature(name,1)
349+
350+
# Then disable the rest, so that off-by-default features don't
351+
# get flagged as errors when they're required by an enabled feature
352+
for name,feature in self.features.items():
353+
if not self.feature_is_included(name):
354+
feature.exclude_from(self)
355+
self._set_feature(name,0)
356+
279357
def get_command_class(self, command):
280358
"""Pluggable version of get_command_class()"""
281359
if command in self.cmdclass:
@@ -295,6 +373,25 @@ def print_commands(self):
295373
self.cmdclass[ep.name] = cmdclass
296374
return _Distribution.print_commands(self)
297375

376+
def _set_feature(self,name,status):
377+
"""Set feature's inclusion status"""
378+
setattr(self,self._feature_attrname(name),status)
379+
380+
def feature_is_included(self,name):
381+
"""Return 1 if feature is included, 0 if excluded, 'None' if unknown"""
382+
return getattr(self,self._feature_attrname(name))
383+
384+
def include_feature(self,name):
385+
"""Request inclusion of feature named 'name'"""
386+
387+
if self.feature_is_included(name)==0:
388+
descr = self.features[name].description
389+
raise DistutilsOptionError(
390+
descr + " is required, but was excluded or is not available"
391+
)
392+
self.features[name].include_in(self)
393+
self._set_feature(name,1)
394+
298395
def include(self,**attrs):
299396
"""Add items to distribution that are named in keyword arguments
300397
@@ -542,3 +639,159 @@ def handle_display_options(self, option_order):
542639
# Install it throughout the distutils
543640
for module in distutils.dist, distutils.core, distutils.cmd:
544641
module.Distribution = Distribution
642+
643+
644+
class Feature:
645+
"""
646+
**deprecated** -- The `Feature` facility was never completely implemented
647+
or supported, `has reported issues
648+
<https://bitbucket.org/pypa/setuptools/issue/58>`_ and will be removed in
649+
a future version.
650+
651+
A subset of the distribution that can be excluded if unneeded/wanted
652+
653+
Features are created using these keyword arguments:
654+
655+
'description' -- a short, human readable description of the feature, to
656+
be used in error messages, and option help messages.
657+
658+
'standard' -- if true, the feature is included by default if it is
659+
available on the current system. Otherwise, the feature is only
660+
included if requested via a command line '--with-X' option, or if
661+
another included feature requires it. The default setting is 'False'.
662+
663+
'available' -- if true, the feature is available for installation on the
664+
current system. The default setting is 'True'.
665+
666+
'optional' -- if true, the feature's inclusion can be controlled from the
667+
command line, using the '--with-X' or '--without-X' options. If
668+
false, the feature's inclusion status is determined automatically,
669+
based on 'availabile', 'standard', and whether any other feature
670+
requires it. The default setting is 'True'.
671+
672+
'require_features' -- a string or sequence of strings naming features
673+
that should also be included if this feature is included. Defaults to
674+
empty list. May also contain 'Require' objects that should be
675+
added/removed from the distribution.
676+
677+
'remove' -- a string or list of strings naming packages to be removed
678+
from the distribution if this feature is *not* included. If the
679+
feature *is* included, this argument is ignored. This argument exists
680+
to support removing features that "crosscut" a distribution, such as
681+
defining a 'tests' feature that removes all the 'tests' subpackages
682+
provided by other features. The default for this argument is an empty
683+
list. (Note: the named package(s) or modules must exist in the base
684+
distribution when the 'setup()' function is initially called.)
685+
686+
other keywords -- any other keyword arguments are saved, and passed to
687+
the distribution's 'include()' and 'exclude()' methods when the
688+
feature is included or excluded, respectively. So, for example, you
689+
could pass 'packages=["a","b"]' to cause packages 'a' and 'b' to be
690+
added or removed from the distribution as appropriate.
691+
692+
A feature must include at least one 'requires', 'remove', or other
693+
keyword argument. Otherwise, it can't affect the distribution in any way.
694+
Note also that you can subclass 'Feature' to create your own specialized
695+
feature types that modify the distribution in other ways when included or
696+
excluded. See the docstrings for the various methods here for more detail.
697+
Aside from the methods, the only feature attributes that distributions look
698+
at are 'description' and 'optional'.
699+
"""
700+
701+
@staticmethod
702+
def warn_deprecated():
703+
warnings.warn(
704+
"Features are deprecated and will be removed in a future "
705+
"version. See http://bitbucket.org/pypa/setuptools/65.",
706+
DeprecationWarning,
707+
stacklevel=3,
708+
)
709+
710+
def __init__(self, description, standard=False, available=True,
711+
optional=True, require_features=(), remove=(), **extras):
712+
self.warn_deprecated()
713+
714+
self.description = description
715+
self.standard = standard
716+
self.available = available
717+
self.optional = optional
718+
if isinstance(require_features,(str,Require)):
719+
require_features = require_features,
720+
721+
self.require_features = [
722+
r for r in require_features if isinstance(r,str)
723+
]
724+
er = [r for r in require_features if not isinstance(r,str)]
725+
if er: extras['require_features'] = er
726+
727+
if isinstance(remove,str):
728+
remove = remove,
729+
self.remove = remove
730+
self.extras = extras
731+
732+
if not remove and not require_features and not extras:
733+
raise DistutilsSetupError(
734+
"Feature %s: must define 'require_features', 'remove', or at least one"
735+
" of 'packages', 'py_modules', etc."
736+
)
737+
738+
def include_by_default(self):
739+
"""Should this feature be included by default?"""
740+
return self.available and self.standard
741+
742+
def include_in(self,dist):
743+
744+
"""Ensure feature and its requirements are included in distribution
745+
746+
You may override this in a subclass to perform additional operations on
747+
the distribution. Note that this method may be called more than once
748+
per feature, and so should be idempotent.
749+
750+
"""
751+
752+
if not self.available:
753+
raise DistutilsPlatformError(
754+
self.description+" is required,"
755+
"but is not available on this platform"
756+
)
757+
758+
dist.include(**self.extras)
759+
760+
for f in self.require_features:
761+
dist.include_feature(f)
762+
763+
def exclude_from(self,dist):
764+
765+
"""Ensure feature is excluded from distribution
766+
767+
You may override this in a subclass to perform additional operations on
768+
the distribution. This method will be called at most once per
769+
feature, and only after all included features have been asked to
770+
include themselves.
771+
"""
772+
773+
dist.exclude(**self.extras)
774+
775+
if self.remove:
776+
for item in self.remove:
777+
dist.exclude_package(item)
778+
779+
def validate(self,dist):
780+
781+
"""Verify that feature makes sense in context of distribution
782+
783+
This method is called by the distribution just before it parses its
784+
command line. It checks to ensure that the 'remove' attribute, if any,
785+
contains only valid package/module names that are present in the base
786+
distribution when 'setup()' is called. You may override it in a
787+
subclass to perform any other required validation of the feature
788+
against a target distribution.
789+
"""
790+
791+
for item in self.remove:
792+
if not dist.has_contents_for(item):
793+
raise DistutilsSetupError(
794+
"%s wants to be able to remove %s, but the distribution"
795+
" doesn't contain any packages or modules under %s"
796+
% (self.description, item, item)
797+
)

0 commit comments

Comments
 (0)