1# Licensed to the Apache Software Foundation (ASF) under one or more 2# contributor license agreements. See the NOTICE file distributed with 3# this work for additional information regarding copyright ownership. 4# The ASF licenses this file to You under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with 6# the License. You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import os 17import sys 18import re 19import fnmatch 20 21import setuptools 22from setuptools import setup 23from distutils.core import Command 24 25try: 26 import epydoc # NOQA 27 has_epydoc = True 28except ImportError: 29 has_epydoc = False 30 31# NOTE: Those functions are intentionally moved in-line to prevent setup.py dependening on any 32# Libcloud code which depends on libraries such as typing, enum, requests, etc. 33# START: Taken From Twisted Python which licensed under MIT license 34# https://github.com/powdahound/twisted/blob/master/twisted/python/dist.py 35# https://github.com/powdahound/twisted/blob/master/LICENSE 36 37# Names that are excluded from globbing results: 38EXCLUDE_NAMES = ['{arch}', 'CVS', '.cvsignore', '_darcs', 39 'RCS', 'SCCS', '.svn'] 40EXCLUDE_PATTERNS = ['*.py[cdo]', '*.s[ol]', '.#*', '*~', '*.py'] 41 42 43def _filter_names(names): 44 """ 45 Given a list of file names, return those names that should be copied. 46 """ 47 names = [n for n in names 48 if n not in EXCLUDE_NAMES] 49 # This is needed when building a distro from a working 50 # copy (likely a checkout) rather than a pristine export: 51 for pattern in EXCLUDE_PATTERNS: 52 names = [n for n in names 53 if not fnmatch.fnmatch(n, pattern) and not n.endswith('.py')] 54 return names 55 56 57def relative_to(base, relativee): 58 """ 59 Gets 'relativee' relative to 'basepath'. 60 61 i.e., 62 63 >>> relative_to('/home/', '/home/radix/') 64 'radix' 65 >>> relative_to('.', '/home/radix/Projects/Twisted') 66 'Projects/Twisted' 67 68 The 'relativee' must be a child of 'basepath'. 69 """ 70 basepath = os.path.abspath(base) 71 relativee = os.path.abspath(relativee) 72 if relativee.startswith(basepath): 73 relative = relativee[len(basepath):] 74 if relative.startswith(os.sep): 75 relative = relative[1:] 76 return os.path.join(base, relative) 77 raise ValueError("%s is not a subpath of %s" % (relativee, basepath)) 78 79 80def get_packages(dname, pkgname=None, results=None, ignore=None, parent=None): 81 """ 82 Get all packages which are under dname. This is necessary for 83 Python 2.2's distutils. Pretty similar arguments to getDataFiles, 84 including 'parent'. 85 """ 86 parent = parent or "" 87 prefix = [] 88 if parent: 89 prefix = [parent] 90 bname = os.path.basename(dname) 91 ignore = ignore or [] 92 if bname in ignore: 93 return [] 94 if results is None: 95 results = [] 96 if pkgname is None: 97 pkgname = [] 98 subfiles = os.listdir(dname) 99 abssubfiles = [os.path.join(dname, x) for x in subfiles] 100 101 if '__init__.py' in subfiles: 102 results.append(prefix + pkgname + [bname]) 103 for subdir in filter(os.path.isdir, abssubfiles): 104 get_packages(subdir, pkgname=pkgname + [bname], 105 results=results, ignore=ignore, 106 parent=parent) 107 res = ['.'.join(result) for result in results] 108 return res 109 110 111def get_data_files(dname, ignore=None, parent=None): 112 """ 113 Get all the data files that should be included in this distutils Project. 114 115 'dname' should be the path to the package that you're distributing. 116 117 'ignore' is a list of sub-packages to ignore. This facilitates 118 disparate package hierarchies. That's a fancy way of saying that 119 the 'twisted' package doesn't want to include the 'twisted.conch' 120 package, so it will pass ['conch'] as the value. 121 122 'parent' is necessary if you're distributing a subpackage like 123 twisted.conch. 'dname' should point to 'twisted/conch' and 'parent' 124 should point to 'twisted'. This ensures that your data_files are 125 generated correctly, only using relative paths for the first element 126 of the tuple ('twisted/conch/*'). 127 The default 'parent' is the current working directory. 128 """ 129 parent = parent or "." 130 ignore = ignore or [] 131 result = [] 132 for directory, subdirectories, filenames in os.walk(dname): 133 resultfiles = [] 134 for exname in EXCLUDE_NAMES: 135 if exname in subdirectories: 136 subdirectories.remove(exname) 137 for ig in ignore: 138 if ig in subdirectories: 139 subdirectories.remove(ig) 140 for filename in _filter_names(filenames): 141 resultfiles.append(filename) 142 if resultfiles: 143 for filename in resultfiles: 144 file_path = os.path.join(directory, filename) 145 if parent: 146 file_path = file_path.replace(parent + os.sep, '') 147 result.append(file_path) 148 149 return result 150# END: Taken from Twisted 151 152 153# Different versions of python have different requirements. We can't use 154# libcloud.utils.py3 here because it relies on backports dependency being 155# installed / available 156PY_pre_35 = sys.version_info < (3, 5, 0) 157 158HTML_VIEWSOURCE_BASE = 'https://svn.apache.org/viewvc/libcloud/trunk' 159PROJECT_BASE_DIR = 'https://libcloud.apache.org' 160TEST_PATHS = ['libcloud/test', 'libcloud/test/common', 'libcloud/test/compute', 161 'libcloud/test/storage', 'libcloud/test/loadbalancer', 162 'libcloud/test/dns', 'libcloud/test/container', 163 'libcloud/test/backup'] 164DOC_TEST_MODULES = ['libcloud.compute.drivers.dummy', 165 'libcloud.storage.drivers.dummy', 166 'libcloud.dns.drivers.dummy', 167 'libcloud.container.drivers.dummy', 168 'libcloud.backup.drivers.dummy'] 169 170SUPPORTED_VERSIONS = ['PyPy 3', 'Python 3.5+'] 171 172# NOTE: python_version syntax is only supported when build system has 173# setuptools >= 36.2 174# For installation, minimum required pip version is 1.4 175# Reference: https://hynek.me/articles/conditional-python-dependencies/ 176# We rely on >= 2.26.0 to avoid issues with LGL transitive dependecy 177# See https://github.com/apache/libcloud/issues/1594 for more context 178INSTALL_REQUIREMENTS = [ 179 'requests>=2.5.0', 180] 181 182setuptools_version = tuple(setuptools.__version__.split(".")[0:2]) 183setuptools_version = tuple([int(c) for c in setuptools_version]) 184 185if setuptools_version < (36, 2): 186 if 'bdist_wheel' in sys.argv: 187 # NOTE: We need to do that because we use universal wheel 188 msg = ('Need to use latest version of setuptools when building wheels to ensure included ' 189 'metadata is correct. Current version: %s' % (setuptools.__version__)) 190 raise RuntimeError(msg) 191 192TEST_REQUIREMENTS = [ 193 'mock', 194 'requests_mock', 195 'pytest', 196 'pytest-runner' 197] + INSTALL_REQUIREMENTS 198 199if PY_pre_35: 200 version = '.'.join([str(x) for x in sys.version_info[:3]]) 201 print('Version ' + version + ' is not supported. Supported versions are: %s. ' 202 'Latest version which supports Python 2.7 and Python 3 < 3.5.0 is ' 203 'Libcloud v2.8.2' % ', '.join(SUPPORTED_VERSIONS)) 204 sys.exit(1) 205 206 207def read_version_string(): 208 version = None 209 cwd = os.path.dirname(os.path.abspath(__file__)) 210 version_file = os.path.join(cwd, 'libcloud/__init__.py') 211 212 with open(version_file) as fp: 213 content = fp.read() 214 215 match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 216 content, re.M) 217 218 if match: 219 version = match.group(1) 220 return version 221 222 raise Exception('Cannot find version in libcloud/__init__.py') 223 224 225def forbid_publish(): 226 argv = sys.argv 227 if 'upload'in argv: 228 print('You shouldn\'t use upload command to upload a release to PyPi. ' 229 'You need to manually upload files generated using release.sh ' 230 'script.\n' 231 'For more information, see "Making a release section" in the ' 232 'documentation') 233 sys.exit(1) 234 235 236class ApiDocsCommand(Command): 237 description = "generate API documentation" 238 user_options = [] 239 240 def initialize_options(self): 241 pass 242 243 def finalize_options(self): 244 pass 245 246 def run(self): 247 if not has_epydoc: 248 raise RuntimeError('Missing "epydoc" package!') 249 250 os.system( 251 'pydoctor' 252 ' --add-package=libcloud' 253 ' --project-name=libcloud' 254 ' --make-html' 255 ' --html-viewsource-base="%s"' 256 ' --project-base-dir=`pwd`' 257 ' --project-url="%s"' 258 % (HTML_VIEWSOURCE_BASE, PROJECT_BASE_DIR)) 259 260 261forbid_publish() 262 263needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) 264pytest_runner = ['pytest-runner'] if needs_pytest else [] 265 266setup( 267 name='apache-libcloud', 268 version=read_version_string(), 269 description='A standard Python library that abstracts away differences' + 270 ' among multiple cloud provider APIs. For more information' + 271 ' and documentation, please see https://libcloud.apache.org', 272 long_description=open('README.rst').read(), 273 author='Apache Software Foundation', 274 author_email='dev@libcloud.apache.org', 275 install_requires=INSTALL_REQUIREMENTS, 276 python_requires=">=3.5, <4", 277 packages=get_packages('libcloud'), 278 package_dir={ 279 'libcloud': 'libcloud', 280 }, 281 package_data={ 282 'libcloud': get_data_files('libcloud', parent='libcloud') + ['py.typed'], 283 }, 284 license='Apache License (2.0)', 285 url='https://libcloud.apache.org/', 286 setup_requires=pytest_runner, 287 tests_require=TEST_REQUIREMENTS, 288 cmdclass={ 289 'apidocs': ApiDocsCommand, 290 }, 291 zip_safe=False, 292 classifiers=[ 293 'Development Status :: 5 - Production/Stable', 294 'Environment :: Console', 295 'Intended Audience :: Developers', 296 'Intended Audience :: System Administrators', 297 'License :: OSI Approved :: Apache Software License', 298 'Operating System :: OS Independent', 299 'Programming Language :: Python', 300 'Topic :: Software Development :: Libraries :: Python Modules', 301 'Programming Language :: Python :: 3', 302 'Programming Language :: Python :: 3.5', 303 'Programming Language :: Python :: 3.6', 304 'Programming Language :: Python :: 3.7', 305 'Programming Language :: Python :: 3.8', 306 'Programming Language :: Python :: 3.9', 307 'Programming Language :: Python :: 3.10', 308 'Programming Language :: Python :: Implementation :: CPython', 309 'Programming Language :: Python :: Implementation :: PyPy' 310 ] 311) 312