33import re
44import os
55import sys
6+ import warnings
67import distutils .log
78import distutils .core
89import distutils .cmd
910from 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
1215from setuptools .compat import numeric_types , basestring
1316import pkg_resources
1417
@@ -134,7 +137,7 @@ def check_packages(dist, attr, value):
134137
135138
136139class 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
543640for 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