# # header # coding: utf-8 from __future__ import print_function, absolute_import, division, unicode_literals # # __init__.py parser import sys import os sys.path = [path for path in sys.path if path not in [os.getcwd(), '']] import platform # NOQA from _ast import * # NOQA from ast import parse # NOQA from setuptools import setup, Extension, Distribution # NOQA from setuptools.command import install_lib # NOQA if __name__ != '__main__': raise NotImplementedError('should never include setup.py') # # definitions full_package_name = None if __name__ != '__main__': raise NotImplementedError('should never include setup.py') if sys.version_info < (3, ): string_type = basestring else: string_type = str if sys.version_info < (3, 4): class Bytes(): pass class NameConstant: pass if sys.version_info < (2, 7) or platform.python_implementation() == 'Jython': class Set(): pass def literal_eval(node_or_string): """ Safely evaluate an expression node or a string containing a Python expression. The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None. """ _safe_names = {'None': None, 'True': True, 'False': False} if isinstance(node_or_string, string_type): node_or_string = parse(node_or_string, mode='eval') if isinstance(node_or_string, Expression): node_or_string = node_or_string.body else: raise TypeError("only string or AST nodes supported") def _convert(node): if isinstance(node, (Str, Bytes)): return node.s elif isinstance(node, Num): return node.n elif isinstance(node, Tuple): return tuple(map(_convert, node.elts)) elif isinstance(node, List): return list(map(_convert, node.elts)) elif isinstance(node, Set): return set(map(_convert, node.elts)) elif isinstance(node, Dict): return dict((_convert(k), _convert(v)) for k, v in zip(node.keys, node.values)) elif isinstance(node, NameConstant): return node.value elif sys.version_info < (3, 4) and isinstance(node, Name): if node.id in _safe_names: return _safe_names[node.id] elif isinstance(node, UnaryOp) and \ isinstance(node.op, (UAdd, USub)) and \ isinstance(node.operand, (Num, UnaryOp, BinOp)): # NOQA operand = _convert(node.operand) if isinstance(node.op, UAdd): return + operand else: return - operand elif isinstance(node, BinOp) and \ isinstance(node.op, (Add, Sub)) and \ isinstance(node.right, (Num, UnaryOp, BinOp)) and \ isinstance(node.left, (Num, UnaryOp, BinOp)): # NOQA left = _convert(node.left) right = _convert(node.right) if isinstance(node.op, Add): return left + right else: return left - right elif isinstance(node, Call): func_id = getattr(node.func, 'id', None) if func_id == 'dict': return dict((k.arg, _convert(k.value)) for k in node.keywords) elif func_id == 'set': return set(_convert(node.args[0])) elif func_id == 'date': return datetime.date(*[_convert(k) for k in node.args]) elif func_id == 'datetime': return datetime.datetime(*[_convert(k) for k in node.args]) err = SyntaxError('malformed node or string: ' + repr(node)) err.filename = '' err.lineno = node.lineno err.offset = node.col_offset err.text = repr(node) err.node = node raise err return _convert(node_or_string) # parses python ( "= dict( )" ) or ( "= {" ) def _package_data(fn): data = {} with open(fn) as fp: parsing = False lines = [] for line in fp.readlines(): if sys.version_info < (3,): line = line.decode('utf-8') if line.startswith(u'_package_data'): if 'dict(' in line: parsing = 'python' lines.append(u'dict(\n') elif line.endswith(u'= {\n'): parsing = 'python' lines.append(u'{\n') else: raise NotImplementedError continue if not parsing: continue if parsing == 'python': if line.startswith(u')') or line.startswith(u'}'): lines.append(line) try: data = literal_eval(u''.join(lines)) except SyntaxError as e: context = 2 from_line = e.lineno - (context + 1) to_line = e.lineno + (context - 1) w = len(str(to_line)) for index, line in enumerate(lines): if from_line <= index <= to_line: print(u"{0:{1}}: {2}".format(index, w, line).encode('utf-8'), end=u'') if index == e.lineno - 1: print(u"{0:{1}} {2}^--- {3}".format( u' ', w, u' ' * e.offset, e.node)) raise break lines.append(line) else: raise NotImplementedError return data # make sure you can run "python ../some/dir/setup.py install" pkg_data = _package_data(__file__.replace('setup.py', '__init__.py')) exclude_files = [ 'setup.py', ] # # helper def _check_convert_version(tup): """Create a PEP 386 pseudo-format conformant string from tuple tup.""" ret_val = str(tup[0]) # first is always digit next_sep = "." # separator for next extension, can be "" or "." nr_digits = 0 # nr of adjacent digits in rest, to verify post_dev = False # are we processig post/dev for x in tup[1:]: if isinstance(x, int): nr_digits += 1 if nr_digits > 2: raise ValueError("too many consecutive digits after " + ret_val) ret_val += next_sep + str(x) next_sep = '.' continue first_letter = x[0].lower() next_sep = '' if first_letter in 'abcr': if post_dev: raise ValueError("release level specified after " "post/dev: " + x) nr_digits = 0 ret_val += 'rc' if first_letter == 'r' else first_letter elif first_letter in 'pd': nr_digits = 1 # only one can follow post_dev = True ret_val += '.post' if first_letter == 'p' else '.dev' else: raise ValueError('First letter of "' + x + '" not recognised') # .dev and .post need a number otherwise setuptools normalizes and complains if nr_digits == 1 and post_dev: ret_val += '0' return ret_val version_info = pkg_data['version_info'] version_str = _check_convert_version(version_info) class MyInstallLib(install_lib.install_lib): def install(self): fpp = pkg_data['full_package_name'].split('.') # full package path full_exclude_files = [os.path.join(*(fpp + [x])) for x in exclude_files] alt_files = [] outfiles = install_lib.install_lib.install(self) for x in outfiles: for full_exclude_file in full_exclude_files: if full_exclude_file in x: os.remove(x) break else: alt_files.append(x) return alt_files class NameSpacePackager(object): def __init__(self, pkg_data): assert isinstance(pkg_data, dict) self._pkg_data = pkg_data self.full_package_name = self.pn(self._pkg_data['full_package_name']) self._split = None self.depth = self.full_package_name.count('.') self.command = None self._pkg = [None, None] # required and pre-installable packages if sys.argv[0] == 'setup.py' and sys.argv[1] == 'install' and \ '--single-version-externally-managed' not in sys.argv: if os.environ.get('READTHEDOCS', None) == 'True': os.system('pip install .') sys.exit(0) print('error: you have to install with "pip install ."') sys.exit(1) # If you only support an extension module on Linux, Windows thinks it # is pure. That way you would get pure python .whl files that take # precedence for downloading on Linux over source with compilable C if self._pkg_data.get('universal'): Distribution.is_pure = lambda *args: True else: Distribution.is_pure = lambda *args: False for x in sys.argv: if x[0] == '-' or x == 'setup.py': continue self.command = x break def pn(self, s): if sys.version_info < (3, ) and isinstance(s, unicode): return s.encode('utf-8') return s @property def split(self): """split the full package name in list of compontents traditionally done by setuptools.find_packages. This routine skips any directories with __init__.py that start with "_" or ".", or contain a setup.py/tox.ini (indicating a subpackage) """ if self._split is None: fpn = self.full_package_name.split('.') self._split = [] while fpn: self._split.insert(0, '.'.join(fpn)) fpn = fpn[:-1] for d in os.listdir('.'): if not os.path.isdir(d) or d == self._split[0] or d[0] in '._': continue # prevent sub-packages in namespace from being included x = os.path.join(d, 'setup.py') if os.path.exists(x): if not os.path.exists(os.path.join(d, 'tox.ini')): print('\n>>>>> found "{0}" without tox.ini <<<<<\n' ''.format(x)) continue x = os.path.join(d, '__init__.py') if os.path.exists(x): self._split.append(self.full_package_name + '.' + d) if sys.version_info < (3, ): self._split = [(y.encode('utf-8') if isinstance(y, unicode) else y) for y in self._split] return self._split @property def namespace_packages(self): return self.split[:self.depth] def namespace_directories(self, depth=None): """return list of directories where the namespace should be created / can be found """ res = [] for index, d in enumerate(self.split[:depth]): # toplevel gets a dot if index > 0: d = os.path.join(*d.split('.')) res.append('.' + d) return res @property def package_dir(self): d = { # don't specify empty dir, clashes with package_data spec self.full_package_name: '.', } if len(self.split) > 1: # only if package namespace d[self.split[0]] = self.namespace_directories(1)[0] return d def create_dirs(self): """create the directories necessary for namespace packaging""" directories = self.namespace_directories(self.depth) if not directories: return if not os.path.exists(directories[0]): for d in directories: os.mkdir(d) with open(os.path.join(d, '__init__.py'), 'w') as fp: fp.write('import pkg_resources\n' 'pkg_resources.declare_namespace(__name__)\n') def check(self): try: from pip.exceptions import InstallationError except ImportError: return # arg is either develop (pip install -e) or install if self.command not in ['install', 'develop']: return # if hgi and hgi.base are both in namespace_packages matching # against the top (hgi.) it suffices to find minus-e and non-minus-e # installed packages. As we don't know the order in namespace_packages # do some magic prefix = self.split[0] prefixes = set([prefix, prefix.replace('_', '-')]) for p in sys.path: if not p: continue # directory with setup.py if os.path.exists(os.path.join(p, 'setup.py')): continue # some linked in stuff might not be hgi based if not os.path.isdir(p): continue if p.startswith('/tmp/'): continue for fn in os.listdir(p): for pre in prefixes: if fn.startswith(pre): break else: continue full_name = os.path.join(p, fn) # not in prefixes the toplevel is never changed from _ to - if fn == prefix and os.path.isdir(full_name): # directory -> other, non-minus-e, install if self.command == 'develop': raise InstallationError( 'Cannot mix develop (pip install -e),\nwith ' 'non-develop installs for package name {0}'.format( fn)) elif fn == prefix: raise InstallationError( 'non directory package {0} in {1}'.format( fn, p)) for pre in [x + '.' for x in prefixes]: if fn.startswith(pre): break else: continue # hgiabc instead of hgi. if fn.endswith('-link') and self.command == 'install': raise InstallationError( 'Cannot mix non-develop with develop\n(pip install -e)' ' installs for package name {0}'.format(fn)) def entry_points(self, script_name=None, package_name=None): """normally called without explicit script_name and package name the default console_scripts entry depends on the existence of __main__.py: if that file exists then the function main() in there is used, otherwise the in __init__.py. the _package_data entry_points key/value pair can be explicitly specified including a "=" character. If the entry is True or 1 the scriptname is the last part of the full package path (split on '.') if the ep entry is a simple string without "=", that is assumed to be the name of the script. """ def pckg_entry_point(name): return '{0}{1}:main'.format( name, '.__main__' if os.path.exists('__main__.py') else '', ) ep = self._pkg_data.get('entry_points', True) if ep is None: return None if ep not in [True, 1]: if '=' in ep: # full specification of the entry point like # entry_points=['yaml = ruamel.yaml.cmd:main'], return {'console_scripts': [ep]} # assume that it is just the script name script_name = ep if package_name is None: package_name = self.full_package_name if not script_name: script_name = package_name.split('.')[-1] return {'console_scripts': [ '{0} = {1}'.format(script_name, pckg_entry_point(package_name)), ]} @property def url(self): if self.full_package_name.startswith('ruamel.'): sp = self.full_package_name.split('.', 1) else: sp = ['ruamel', self.full_package_name] return 'https://bitbucket.org/{0}/{1}'.format(*sp) @property def author(self): return self._pkg_data['author'] @property def author_email(self): return self._pkg_data['author_email'] @property def license(self): """return the license field from _package_data, None means MIT""" lic = self._pkg_data.get('license') if lic is None: # lic_fn = os.path.join(os.path.dirname(__file__), 'LICENSE') # assert os.path.exists(lic_fn) return "MIT license" return lic def has_mit_lic(self): return 'MIT' in self.license @property def description(self): return self._pkg_data['description'] @property def status(self): # αβ status = self._pkg_data.get('status', u'β').lower() if status in [u'α', u'alpha']: return (3, 'Alpha') elif status in [u'β', u'beta']: return (4, 'Beta') elif u'stable' in status.lower(): return (5, 'Production/Stable') raise NotImplementedError @property def classifiers(self): return [ 'Development Status :: {0} - {1}'.format(*self.status), 'Intended Audience :: Developers', 'License :: ' + ('OSI Approved :: MIT' if self.has_mit_lic() else 'Other/Proprietary') + ' License', 'Operating System :: OS Independent', 'Programming Language :: Python', ] + [self.pn(x) for x in self._pkg_data.get('classifiers', [])] @property def keywords(self): return self.pn(self._pkg_data.get('keywords')) @property def install_requires(self): """list of packages required for installation""" return self._analyse_packages[0] @property def install_pre(self): """list of packages required for installation""" return self._analyse_packages[1] @property def _analyse_packages(self): """gather from configuration, names starting with * need to be installed explicitly as they are not on PyPI install_requires should be dict, with keys 'any', 'py27' etc or a list (which is as if only 'any' was defined """ if self._pkg[0] is None: self._pkg[0] = [] self._pkg[1] = [] ir = self._pkg_data.get('install_requires') if ir is None: return self._pkg # these will be both empty at this point if isinstance(ir, list): self._pkg[0] = ir return self._pkg # 'any' for all builds, 'py27' etc for specifics versions packages = ir.get('any', []) implementation = platform.python_implementation() if implementation == 'CPython': pyver = 'py{0}{1}'.format(*sys.version_info) elif implementation == 'PyPy': pyver = 'pypy' if sys.version_info < (3, ) else 'pypy3' elif implementation == 'Jython': pyver = 'jython' packages.extend(ir.get(pyver, [])) for p in packages: # package name starting with * means use local source tree, non-published # to PyPi or maybe not latest version on PyPI -> pre-install if p[0] == '*': p = p[1:] self._pkg[1].append(p) self._pkg[0].append(p) return self._pkg @property def data_files(self): df = self._pkg_data.get('data_files', []) if self.has_mit_lic(): df.append('LICENSE') if not df: return None return [('.', df), ] @property def package_data(self): df = self._pkg_data.get('data_files', []) if self.has_mit_lic(): # include the file df.append('LICENSE') # but don't install it exclude_files.append('LICENSE') if not df: return {} return {self.full_package_name: df} @property def ext_modules(self): """check if the C module can be build by trying to compile a small program against the libyaml development library""" if hasattr(self, '_ext_modules'): return self._ext_modules if '--version' in sys.argv: return None if platform.python_implementation() == 'Jython': return None if sys.platform == "win32" and not self._pkg_data.get('win32bin'): return None try: plat = sys.argv.index('--plat-name') if 'win' in sys.argv[plat+1]: return None except ValueError: pass import tempfile import shutil from textwrap import dedent import distutils.sysconfig import distutils.ccompiler from distutils.errors import CompileError, LinkError self._ext_modules = [] for target in self._pkg_data.get('ext_modules', []): # list of dicts test_code = target.get('test') libraries = [self.pn(x) for x in target.get('lib')] ext = Extension( self.pn(target['name']), sources=[self.pn(x) for x in target['src']], libraries=libraries, ) if not test_code: self._ext_modules.append(ext) continue # write a temporary .c file to compile c_code = dedent(target['test']) try: tmp_dir = tempfile.mkdtemp(prefix='tmp_ruamel_') bin_file_name = 'test' + self.pn(target['name']) file_name = os.path.join(tmp_dir, bin_file_name + '.c') with open(file_name, 'w') as fp: fp.write(c_code) # and try to compile it compiler = distutils.ccompiler.new_compiler() assert isinstance(compiler, distutils.ccompiler.CCompiler) distutils.sysconfig.customize_compiler(compiler) try: compiler.link_executable( compiler.compile( [file_name], output_dir='/', # as file_name has absolute prefix ), bin_file_name, output_dir=tmp_dir, libraries=libraries, ) except CompileError: print('compile error:', file_name) continue except LinkError: print('libyaml link error', file_name) continue self._ext_modules.append(ext) except Exception as e: # NOQA # print('Exception:', e) pass finally: shutil.rmtree(tmp_dir) return self._ext_modules def wheel(self, kw, setup): """temporary add setup.cfg if creating a wheel to include LICENSE file https://bitbucket.org/pypa/wheel/issues/47 """ if 'bdist_wheel' not in sys.argv: return file_name = 'setup.cfg' if os.path.exists(file_name): # add it if not in there? return with open(file_name, 'w') as fp: if os.path.exists('LICENSE'): fp.write('[metadata]\nlicense-file = LICENSE\n') else: print("\n\n>>>>>> LICENSE file not found <<<<<\n\n") if self._pkg_data.get('universal'): fp.write('[bdist_wheel]\nuniversal = 1\n') try: setup(**kw) except: raise finally: os.remove(file_name) return True # # call setup def main(): dump_kw = '--dump-kw' if dump_kw in sys.argv: import wheel import distutils print('python: ', sys.version) print('distutils:', distutils.__version__) print('wheel: ', wheel.__version__) nsp = NameSpacePackager(pkg_data) nsp.check() nsp.create_dirs() kw = dict( name=nsp.full_package_name, namespace_packages=nsp.namespace_packages, version=version_str, packages=nsp.split, url=nsp.url, author=nsp.author, author_email=nsp.author_email, cmdclass={'install_lib': MyInstallLib}, package_dir=nsp.package_dir, entry_points=nsp.entry_points(), description=nsp.description, install_requires=nsp.install_requires, license=nsp.license, classifiers=nsp.classifiers, keywords=nsp.keywords, package_data=nsp.package_data, ext_modules=nsp.ext_modules, ) if '--version' not in sys.argv and ('--verbose' in sys.argv or dump_kw in sys.argv): for k in sorted(kw): v = kw[k] print(' "{0}": "{1}",'.format(k, v)) if dump_kw in sys.argv: sys.argv.remove(dump_kw) with open('README.rst') as fp: kw['long_description'] = fp.read() if nsp.wheel(kw, setup): return for x in ['-c', 'egg_info', '--egg-base', 'pip-egg-info']: if x not in sys.argv: break else: # we're doing a tox setup install any starred package by searching up the source tree # until you match your/package/name for your.package.name for p in nsp.install_pre: import subprocess # search other source setup_path = os.path.join(*p.split('.') + ['setup.py']) try_dir = os.path.dirname(sys.executable) while len(try_dir) > 1: full_path_setup_py = os.path.join(try_dir, setup_path) if os.path.exists(full_path_setup_py): pip = sys.executable.replace('python', 'pip') cmd = [pip, 'install', os.path.dirname(full_path_setup_py)] # with open('/var/tmp/notice', 'a') as fp: # print('installing', cmd, file=fp) subprocess.check_output(cmd) break try_dir = os.path.dirname(try_dir) setup(**kw) main()