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