123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- #!/usr/bin/env python
- # coding: utf-8
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- """
- This file originates from the 'jupyter-packaging' package, and
- contains a set of useful utilities for including npm packages
- within a Python package.
- """
- from collections import defaultdict
- from os.path import join as pjoin
- from shutil import which
- import io
- import os
- import functools
- import pipes
- import re
- import shlex
- import subprocess
- import sys
- # BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
- # update it when the contents of directories change.
- if os.path.exists('MANIFEST'): os.remove('MANIFEST') # noqa
- from distutils.cmd import Command
- from distutils.command.build_py import build_py
- from distutils.command.sdist import sdist
- from distutils import log
- from setuptools.command.develop import develop
- from setuptools.command.bdist_egg import bdist_egg
- try:
- from wheel.bdist_wheel import bdist_wheel
- except ImportError:
- bdist_wheel = None
- if sys.platform == 'win32':
- from subprocess import list2cmdline
- else:
- def list2cmdline(cmd_list):
- return ' '.join(map(pipes.quote, cmd_list))
- __version__ = '0.2.0'
- # ---------------------------------------------------------------------------
- # Top Level Variables
- # ---------------------------------------------------------------------------
- HERE = os.path.abspath(os.path.dirname(__file__))
- is_repo = os.path.exists(pjoin(HERE, '.git'))
- node_modules = pjoin(HERE, 'node_modules')
- SEPARATORS = os.sep if os.altsep is None else os.sep + os.altsep
- npm_path = ':'.join([
- pjoin(HERE, 'node_modules', '.bin'),
- os.environ.get('PATH', os.defpath),
- ])
- if "--skip-npm" in sys.argv:
- print("Skipping npm install as requested.")
- skip_npm = True
- sys.argv.remove("--skip-npm")
- else:
- skip_npm = False
- # ---------------------------------------------------------------------------
- # Public Functions
- # ---------------------------------------------------------------------------
- def get_version(file, name='__version__'):
- """Get the version of the package from the given file by
- executing it and extracting the given `name`.
- """
- path = os.path.realpath(file)
- version_ns = {}
- with io.open(path, encoding="utf8") as f:
- exec(f.read(), {}, version_ns)
- return version_ns[name]
- def ensure_python(specs):
- """Given a list of range specifiers for python, ensure compatibility.
- """
- if not isinstance(specs, (list, tuple)):
- specs = [specs]
- v = sys.version_info
- part = '%s.%s' % (v.major, v.minor)
- for spec in specs:
- if part == spec:
- return
- try:
- if eval(part + spec):
- return
- except SyntaxError:
- pass
- raise ValueError('Python version %s unsupported' % part)
- def find_packages(top=HERE):
- """
- Find all of the packages.
- """
- packages = []
- for d, dirs, _ in os.walk(top, followlinks=True):
- if os.path.exists(pjoin(d, '__init__.py')):
- packages.append(os.path.relpath(d, top).replace(os.path.sep, '.'))
- elif d != top:
- # Don't look for packages in subfolders if current isn't a package.
- dirs[:] = []
- return packages
- def update_package_data(distribution):
- """update build_py options to get package_data changes"""
- build_py = distribution.get_command_obj('build_py')
- build_py.finalize_options()
- class bdist_egg_disabled(bdist_egg):
- """Disabled version of bdist_egg
- Prevents setup.py install performing setuptools' default easy_install,
- which it should never ever do.
- """
- def run(self):
- sys.exit("Aborting implicit building of eggs. Use `pip install .` "
- " to install from source.")
- def create_cmdclass(prerelease_cmd=None, package_data_spec=None,
- data_files_spec=None, exclude=None):
- """Create a command class with the given optional prerelease class.
- Parameters
- ----------
- prerelease_cmd: (name, Command) tuple, optional
- The command to run before releasing.
- package_data_spec: dict, optional
- A dictionary whose keys are the dotted package names and
- whose values are a list of glob patterns.
- data_files_spec: list, optional
- A list of (path, dname, pattern) tuples where the path is the
- `data_files` install path, dname is the source directory, and the
- pattern is a glob pattern.
- exclude: function
- A function which takes a string filename and returns True if the
- file should be excluded from package data and data files, False otherwise.
- Notes
- -----
- We use specs so that we can find the files *after* the build
- command has run.
- The package data glob patterns should be relative paths from the package
- folder containing the __init__.py file, which is given as the package
- name.
- e.g. `dict(foo=['./bar/*', './baz/**'])`
- The data files directories should be absolute paths or relative paths
- from the root directory of the repository. Data files are specified
- differently from `package_data` because we need a separate path entry
- for each nested folder in `data_files`, and this makes it easier to
- parse.
- e.g. `('share/foo/bar', 'pkgname/bizz, '*')`
- """
- wrapped = [prerelease_cmd] if prerelease_cmd else []
- if package_data_spec or data_files_spec:
- wrapped.append('handle_files')
- wrapper = functools.partial(_wrap_command, wrapped)
- handle_files = _get_file_handler(package_data_spec, data_files_spec, exclude)
- if 'bdist_egg' in sys.argv:
- egg = wrapper(bdist_egg, strict=True)
- else:
- egg = bdist_egg_disabled
- cmdclass = dict(
- build_py=wrapper(build_py, strict=is_repo),
- bdist_egg=egg,
- sdist=wrapper(sdist, strict=True),
- handle_files=handle_files,
- )
- if bdist_wheel:
- cmdclass['bdist_wheel'] = wrapper(bdist_wheel, strict=True)
- cmdclass['develop'] = wrapper(develop, strict=True)
- return cmdclass
- def command_for_func(func):
- """Create a command that calls the given function."""
- class FuncCommand(BaseCommand):
- def run(self):
- func()
- update_package_data(self.distribution)
- return FuncCommand
- def run(cmd, **kwargs):
- """Echo a command before running it. Defaults to repo as cwd"""
- log.info('> ' + list2cmdline(cmd))
- kwargs.setdefault('cwd', HERE)
- kwargs.setdefault('shell', os.name == 'nt')
- if not isinstance(cmd, (list, tuple)) and os.name != 'nt':
- cmd = shlex.split(cmd)
- cmd[0] = which(cmd[0])
- return subprocess.check_call(cmd, **kwargs)
- def is_stale(target, source):
- """Test whether the target file/directory is stale based on the source
- file/directory.
- """
- if not os.path.exists(target):
- return True
- target_mtime = recursive_mtime(target) or 0
- return compare_recursive_mtime(source, cutoff=target_mtime)
- class BaseCommand(Command):
- """Empty command because Command needs subclasses to override too much"""
- user_options = []
- def initialize_options(self):
- pass
- def finalize_options(self):
- pass
- def get_inputs(self):
- return []
- def get_outputs(self):
- return []
- def combine_commands(*commands):
- """Return a Command that combines several commands."""
- class CombinedCommand(Command):
- user_options = []
- def initialize_options(self):
- self.commands = []
- for C in commands:
- self.commands.append(C(self.distribution))
- for c in self.commands:
- c.initialize_options()
- def finalize_options(self):
- for c in self.commands:
- c.finalize_options()
- def run(self):
- for c in self.commands:
- c.run()
- return CombinedCommand
- def compare_recursive_mtime(path, cutoff, newest=True):
- """Compare the newest/oldest mtime for all files in a directory.
- Cutoff should be another mtime to be compared against. If an mtime that is
- newer/older than the cutoff is found it will return True.
- E.g. if newest=True, and a file in path is newer than the cutoff, it will
- return True.
- """
- if os.path.isfile(path):
- mt = mtime(path)
- if newest:
- if mt > cutoff:
- return True
- elif mt < cutoff:
- return True
- for dirname, _, filenames in os.walk(path, topdown=False):
- for filename in filenames:
- mt = mtime(pjoin(dirname, filename))
- if newest: # Put outside of loop?
- if mt > cutoff:
- return True
- elif mt < cutoff:
- return True
- return False
- def recursive_mtime(path, newest=True):
- """Gets the newest/oldest mtime for all files in a directory."""
- if os.path.isfile(path):
- return mtime(path)
- current_extreme = None
- for dirname, dirnames, filenames in os.walk(path, topdown=False):
- for filename in filenames:
- mt = mtime(pjoin(dirname, filename))
- if newest: # Put outside of loop?
- if mt >= (current_extreme or mt):
- current_extreme = mt
- elif mt <= (current_extreme or mt):
- current_extreme = mt
- return current_extreme
- def mtime(path):
- """shorthand for mtime"""
- return os.stat(path).st_mtime
- def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build',
- force=False, npm=None):
- """Return a Command for managing an npm installation.
- Note: The command is skipped if the `--skip-npm` flag is used.
- Parameters
- ----------
- path: str, optional
- The base path of the node package. Defaults to the repo root.
- build_dir: str, optional
- The target build directory. If this and source_dir are given,
- the JavaScript will only be build if necessary.
- source_dir: str, optional
- The source code directory.
- build_cmd: str, optional
- The npm command to build assets to the build_dir.
- npm: str or list, optional.
- The npm executable name, or a tuple of ['node', executable].
- """
- class NPM(BaseCommand):
- description = 'install package.json dependencies using npm'
- def run(self):
- if skip_npm:
- log.info('Skipping npm-installation')
- return
- node_package = path or HERE
- node_modules = pjoin(node_package, 'node_modules')
- is_yarn = os.path.exists(pjoin(node_package, 'yarn.lock'))
- npm_cmd = npm
- if npm is None:
- if is_yarn:
- npm_cmd = ['yarn']
- else:
- npm_cmd = ['npm']
- if not which(npm_cmd[0]):
- log.error("`{0}` unavailable. If you're running this command "
- "using sudo, make sure `{0}` is available to sudo"
- .format(npm_cmd[0]))
- return
- stale_package = is_stale(node_modules,
- pjoin(node_package, 'package.json'))
- if force or stale_package:
- log.info('Installing build dependencies with npm. This may '
- 'take a while...')
- run(npm_cmd + ['install'], cwd=node_package)
- if build_dir and source_dir and not force:
- should_build = is_stale(build_dir, source_dir)
- else:
- should_build = True
- if should_build:
- run(npm_cmd + ['run', build_cmd], cwd=node_package)
- return NPM
- def ensure_targets(targets):
- """Return a Command that checks that certain files exist.
- Raises a ValueError if any of the files are missing.
- Note: The check is skipped if the `--skip-npm` flag is used.
- """
- class TargetsCheck(BaseCommand):
- def run(self):
- if skip_npm:
- log.info('Skipping target checks')
- return
- missing = [t for t in targets if not os.path.exists(t)]
- if missing:
- raise ValueError(('missing files: %s' % missing))
- return TargetsCheck
- # ---------------------------------------------------------------------------
- # Private Functions
- # ---------------------------------------------------------------------------
- def _wrap_command(cmds, cls, strict=True):
- """Wrap a setup command
- Parameters
- ----------
- cmds: list(str)
- The names of the other commands to run prior to the command.
- strict: boolean, optional
- Whether to raise errors when a pre-command fails.
- """
- class WrappedCommand(cls):
- def run(self):
- if not getattr(self, 'uninstall', None):
- try:
- [self.run_command(cmd) for cmd in cmds]
- except Exception:
- if strict:
- raise
- else:
- pass
- # update package data
- update_package_data(self.distribution)
- result = cls.run(self)
- return result
- return WrappedCommand
- def _get_file_handler(package_data_spec, data_files_spec, exclude=None):
- """Get a package_data and data_files handler command.
- """
- class FileHandler(BaseCommand):
- def run(self):
- package_data = self.distribution.package_data
- package_spec = package_data_spec or dict()
- for (key, patterns) in package_spec.items():
- files = _get_package_data(key, patterns)
- if exclude is not None:
- files = [f for f in files if not exclude(f)]
- package_data[key] = files
- self.distribution.data_files = _get_data_files(
- data_files_spec, self.distribution.data_files, exclude
- )
- return FileHandler
- def _get_data_files(data_specs, existing, exclude=None):
- """Expand data file specs into valid data files metadata.
- Parameters
- ----------
- data_specs: list of tuples
- See [createcmdclass] for description.
- existing: list of tuples
- The existing distribution data_files metadata.
- Returns
- -------
- A valid list of data_files items.
- """
- # Extract the existing data files into a staging object.
- file_data = defaultdict(list)
- for (path, files) in existing or []:
- file_data[path] = files
- # Extract the files and assign them to the proper data
- # files path.
- for (path, dname, pattern) in data_specs or []:
- dname = dname.replace(os.sep, '/')
- offset = len(dname) + 1
- files = _get_files(pjoin(dname, pattern))
- for fname in files:
- # Normalize the path.
- root = os.path.dirname(fname)
- full_path = '/'.join([path, root[offset:]])
- if full_path.endswith('/'):
- full_path = full_path[:-1]
- if exclude is not None and exclude(fname):
- continue
- file_data[full_path].append(fname)
- # Construct the data files spec.
- data_files = []
- for (path, files) in file_data.items():
- data_files.append((path, files))
- return data_files
- def _get_files(file_patterns, top=HERE):
- """Expand file patterns to a list of paths.
- Parameters
- -----------
- file_patterns: list or str
- A list of glob patterns for the data file locations.
- The globs can be recursive if they include a `**`.
- They should be relative paths from the top directory or
- absolute paths.
- top: str
- the directory to consider for data files
- Note:
- Files in `node_modules` are ignored.
- """
- if not isinstance(file_patterns, (list, tuple)):
- file_patterns = [file_patterns]
- for i, p in enumerate(file_patterns):
- if os.path.isabs(p):
- file_patterns[i] = os.path.relpath(p, top)
- matchers = [_compile_pattern(p) for p in file_patterns]
- files = set()
- for root, dirnames, filenames in os.walk(top):
- # Don't recurse into node_modules
- if 'node_modules' in dirnames:
- dirnames.remove('node_modules')
- for m in matchers:
- for filename in filenames:
- fn = os.path.relpath(pjoin(root, filename), top)
- if m(fn):
- files.add(fn.replace(os.sep, '/'))
- return list(files)
- def _get_package_data(root, file_patterns=None):
- """Expand file patterns to a list of `package_data` paths.
- Parameters
- -----------
- root: str
- The relative path to the package root from `HERE`.
- file_patterns: list or str, optional
- A list of glob patterns for the data file locations.
- The globs can be recursive if they include a `**`.
- They should be relative paths from the root or
- absolute paths. If not given, all files will be used.
- Note:
- Files in `node_modules` are ignored.
- """
- if file_patterns is None:
- file_patterns = ['*']
- return _get_files(file_patterns, pjoin(HERE, root))
- def _compile_pattern(pat, ignore_case=True):
- """Translate and compile a glob pattern to a regular expression matcher."""
- if isinstance(pat, bytes):
- pat_str = pat.decode('ISO-8859-1')
- res_str = _translate_glob(pat_str)
- res = res_str.encode('ISO-8859-1')
- else:
- res = _translate_glob(pat)
- flags = re.IGNORECASE if ignore_case else 0
- return re.compile(res, flags=flags).match
- def _iexplode_path(path):
- """Iterate over all the parts of a path.
- Splits path recursively with os.path.split().
- """
- (head, tail) = os.path.split(path)
- if not head or (not tail and head == path):
- if head:
- yield head
- if tail or not head:
- yield tail
- return
- for p in _iexplode_path(head):
- yield p
- yield tail
- def _translate_glob(pat):
- """Translate a glob PATTERN to a regular expression."""
- translated_parts = []
- for part in _iexplode_path(pat):
- translated_parts.append(_translate_glob_part(part))
- os_sep_class = '[%s]' % re.escape(SEPARATORS)
- res = _join_translated(translated_parts, os_sep_class)
- return '{res}\\Z(?ms)'.format(res=res)
- def _join_translated(translated_parts, os_sep_class):
- """Join translated glob pattern parts.
- This is different from a simple join, as care need to be taken
- to allow ** to match ZERO or more directories.
- """
- res = ''
- for part in translated_parts[:-1]:
- if part == '.*':
- # drop separator, since it is optional
- # (** matches ZERO or more dirs)
- res += part
- else:
- res += part + os_sep_class
- if translated_parts[-1] == '.*':
- # Final part is **
- res += '.+'
- # Follow stdlib/git convention of matching all sub files/directories:
- res += '({os_sep_class}?.*)?'.format(os_sep_class=os_sep_class)
- else:
- res += translated_parts[-1]
- return res
- def _translate_glob_part(pat):
- """Translate a glob PATTERN PART to a regular expression."""
- # Code modified from Python 3 standard lib fnmatch:
- if pat == '**':
- return '.*'
- i, n = 0, len(pat)
- res = []
- while i < n:
- c = pat[i]
- i = i + 1
- if c == '*':
- # Match anything but path separators:
- res.append('[^%s]*' % SEPARATORS)
- elif c == '?':
- res.append('[^%s]?' % SEPARATORS)
- elif c == '[':
- j = i
- if j < n and pat[j] == '!':
- j = j + 1
- if j < n and pat[j] == ']':
- j = j + 1
- while j < n and pat[j] != ']':
- j = j + 1
- if j >= n:
- res.append('\\[')
- else:
- stuff = pat[i:j].replace('\\', '\\\\')
- i = j + 1
- if stuff[0] == '!':
- stuff = '^' + stuff[1:]
- elif stuff[0] == '^':
- stuff = '\\' + stuff
- res.append('[%s]' % stuff)
- else:
- res.append(re.escape(c))
- return ''.join(res)
|