| #! /usr/bin/env python |
| |
| from __future__ import print_function |
| import io |
| import sys |
| import os |
| from os.path import isfile, join as pjoin |
| from glob import glob |
| from setuptools import setup, find_packages, Command |
| from distutils import log |
| from distutils.util import convert_path |
| import subprocess as sp |
| import contextlib |
| import re |
| |
| # Force distutils to use py_compile.compile() function with 'doraise' argument |
| # set to True, in order to raise an exception on compilation errors |
| import py_compile |
| orig_py_compile = py_compile.compile |
| |
| def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): |
| orig_py_compile(file, cfile=cfile, dfile=dfile, doraise=True) |
| |
| py_compile.compile = doraise_py_compile |
| |
| needs_wheel = {'bdist_wheel'}.intersection(sys.argv) |
| wheel = ['wheel'] if needs_wheel else [] |
| needs_bumpversion = {'release'}.intersection(sys.argv) |
| bumpversion = ['bump2version'] if needs_bumpversion else [] |
| |
| extras_require = { |
| # for fontTools.ufoLib: to read/write UFO fonts |
| "ufo": [ |
| "fs >= 2.2.0, < 3", |
| "enum34 >= 1.1.6; python_version < '3.4'", |
| ], |
| # for fontTools.misc.etree and fontTools.misc.plistlib: use lxml to |
| # read/write XML files (faster/safer than built-in ElementTree) |
| "lxml": [ |
| "lxml >= 4.0, < 5", |
| "singledispatch >= 3.4.0.3; python_version < '3.4'", |
| # typing >= 3.6.4 is required when using ABC collections with the |
| # singledispatch backport, see: |
| # https://github.com/fonttools/fonttools/issues/1423 |
| # https://github.com/python/typing/issues/484 |
| "typing >= 3.6.4; python_version < '3.4'", |
| ], |
| # for fontTools.sfnt and fontTools.woff2: to compress/uncompress |
| # WOFF 1.0 and WOFF 2.0 webfonts. |
| "woff": [ |
| "brotli >= 1.0.1; platform_python_implementation != 'PyPy'", |
| "brotlipy >= 0.7.0; platform_python_implementation == 'PyPy'", |
| "zopfli >= 0.1.4", |
| ], |
| # for fontTools.unicode and fontTools.unicodedata: to use the latest version |
| # of the Unicode Character Database instead of the built-in unicodedata |
| # which varies between python versions and may be outdated. |
| "unicode": [ |
| # the unicodedata2 extension module doesn't work on PyPy. |
| # Python 3.8 already has Unicode 12, so the backport is not needed. |
| ( |
| "unicodedata2 >= 12.0.0; " |
| "python_version < '3.8' and platform_python_implementation != 'PyPy'" |
| ), |
| ], |
| # for graphite type tables in ttLib/tables (Silf, Glat, Gloc) |
| "graphite": [ |
| "lz4 >= 1.7.4.2" |
| ], |
| # for fontTools.interpolatable: to solve the "minimum weight perfect |
| # matching problem in bipartite graphs" (aka Assignment problem) |
| "interpolatable": [ |
| # use pure-python alternative on pypy |
| "scipy; platform_python_implementation != 'PyPy'", |
| "munkres; platform_python_implementation == 'PyPy'", |
| ], |
| # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting |
| # VariationModel |
| "plot": [ |
| # TODO: figure out the minimum version of matplotlib that we need |
| "matplotlib", |
| ], |
| # for fontTools.misc.symfont, module for symbolic font statistics analysis |
| "symfont": [ |
| "sympy", |
| ], |
| # To get file creator and type of Macintosh PostScript Type 1 fonts (macOS only) |
| "type1": [ |
| "xattr; sys_platform == 'darwin'", |
| ], |
| } |
| # use a special 'all' key as shorthand to includes all the extra dependencies |
| extras_require["all"] = sum(extras_require.values(), []) |
| |
| |
| # Trove classifiers for PyPI |
| classifiers = {"classifiers": [ |
| "Development Status :: 5 - Production/Stable", |
| "Environment :: Console", |
| "Environment :: Other Environment", |
| "Intended Audience :: Developers", |
| "Intended Audience :: End Users/Desktop", |
| "License :: OSI Approved :: MIT License", |
| "Natural Language :: English", |
| "Operating System :: OS Independent", |
| "Programming Language :: Python", |
| "Programming Language :: Python :: 2", |
| "Programming Language :: Python :: 3", |
| "Topic :: Text Processing :: Fonts", |
| "Topic :: Multimedia :: Graphics", |
| "Topic :: Multimedia :: Graphics :: Graphics Conversion", |
| ]} |
| |
| |
| # concatenate README.rst and NEWS.rest into long_description so they are |
| # displayed on the FontTols project page on PyPI |
| with io.open("README.rst", "r", encoding="utf-8") as readme: |
| long_description = readme.read() |
| long_description += "\nChangelog\n~~~~~~~~~\n\n" |
| with io.open("NEWS.rst", "r", encoding="utf-8") as changelog: |
| long_description += changelog.read() |
| |
| |
| @contextlib.contextmanager |
| def capture_logger(name): |
| """ Context manager to capture a logger output with a StringIO stream. |
| """ |
| import logging |
| |
| logger = logging.getLogger(name) |
| try: |
| import StringIO |
| stream = StringIO.StringIO() |
| except ImportError: |
| stream = io.StringIO() |
| handler = logging.StreamHandler(stream) |
| logger.addHandler(handler) |
| try: |
| yield stream |
| finally: |
| logger.removeHandler(handler) |
| |
| |
| class release(Command): |
| """ |
| Tag a new release with a single command, using the 'bumpversion' tool |
| to update all the version strings in the source code. |
| The version scheme conforms to 'SemVer' and PEP 440 specifications. |
| |
| Firstly, the pre-release '.devN' suffix is dropped to signal that this is |
| a stable release. If '--major' or '--minor' options are passed, the |
| the first or second 'semver' digit is also incremented. Major is usually |
| for backward-incompatible API changes, while minor is used when adding |
| new backward-compatible functionalities. No options imply 'patch' or bug-fix |
| release. |
| |
| A new header is also added to the changelog file ("NEWS.rst"), containing |
| the new version string and the current 'YYYY-MM-DD' date. |
| |
| All changes are committed, and an annotated git tag is generated. With the |
| --sign option, the tag is GPG-signed with the user's default key. |
| |
| Finally, the 'patch' part of the version string is bumped again, and a |
| pre-release suffix '.dev0' is appended to mark the opening of a new |
| development cycle. |
| |
| Links: |
| - http://semver.org/ |
| - https://www.python.org/dev/peps/pep-0440/ |
| - https://github.com/c4urself/bump2version |
| """ |
| |
| description = "update version strings for release" |
| |
| user_options = [ |
| ("major", None, "bump the first digit (incompatible API changes)"), |
| ("minor", None, "bump the second digit (new backward-compatible features)"), |
| ("sign", "s", "make a GPG-signed tag, using the default key"), |
| ("allow-dirty", None, "don't abort if working directory is dirty"), |
| ] |
| |
| changelog_name = "NEWS.rst" |
| version_RE = re.compile("^[0-9]+\.[0-9]+") |
| date_fmt = u"%Y-%m-%d" |
| header_fmt = u"%s (released %s)" |
| commit_message = "Release {new_version}" |
| tag_name = "{new_version}" |
| version_files = [ |
| "setup.cfg", |
| "setup.py", |
| "Lib/fontTools/__init__.py", |
| ] |
| |
| def initialize_options(self): |
| self.minor = False |
| self.major = False |
| self.sign = False |
| self.allow_dirty = False |
| |
| def finalize_options(self): |
| if all([self.major, self.minor]): |
| from distutils.errors import DistutilsOptionError |
| raise DistutilsOptionError("--major/--minor are mutually exclusive") |
| self.part = "major" if self.major else "minor" if self.minor else None |
| |
| def run(self): |
| if self.part is not None: |
| log.info("bumping '%s' version" % self.part) |
| self.bumpversion(self.part, commit=False) |
| release_version = self.bumpversion( |
| "release", commit=False, allow_dirty=True) |
| else: |
| log.info("stripping pre-release suffix") |
| release_version = self.bumpversion("release") |
| log.info(" version = %s" % release_version) |
| |
| changes = self.format_changelog(release_version) |
| |
| self.git_commit(release_version) |
| self.git_tag(release_version, changes, self.sign) |
| |
| log.info("bumping 'patch' version and pre-release suffix") |
| next_dev_version = self.bumpversion('patch', commit=True) |
| log.info(" version = %s" % next_dev_version) |
| |
| def git_commit(self, version): |
| """ Stage and commit all relevant version files, and format the commit |
| message with specified 'version' string. |
| """ |
| files = self.version_files + [self.changelog_name] |
| |
| log.info("committing changes") |
| for f in files: |
| log.info(" %s" % f) |
| if self.dry_run: |
| return |
| sp.check_call(["git", "add"] + files) |
| msg = self.commit_message.format(new_version=version) |
| sp.check_call(["git", "commit", "-m", msg], stdout=sp.PIPE) |
| |
| def git_tag(self, version, message, sign=False): |
| """ Create annotated git tag with given 'version' and 'message'. |
| Optionally 'sign' the tag with the user's GPG key. |
| """ |
| log.info("creating %s git tag '%s'" % ( |
| "signed" if sign else "annotated", version)) |
| if self.dry_run: |
| return |
| # create an annotated (or signed) tag from the new version |
| tag_opt = "-s" if sign else "-a" |
| tag_name = self.tag_name.format(new_version=version) |
| proc = sp.Popen( |
| ["git", "tag", tag_opt, "-F", "-", tag_name], stdin=sp.PIPE) |
| # use the latest changes from the changelog file as the tag message |
| tag_message = u"%s\n\n%s" % (tag_name, message) |
| proc.communicate(tag_message.encode('utf-8')) |
| if proc.returncode != 0: |
| sys.exit(proc.returncode) |
| |
| def bumpversion(self, part, commit=False, message=None, allow_dirty=None): |
| """ Run bumpversion.main() with the specified arguments, and return the |
| new computed version string (cf. 'bumpversion --help' for more info) |
| """ |
| import bumpversion |
| |
| args = ( |
| (['--verbose'] if self.verbose > 1 else []) + |
| (['--dry-run'] if self.dry_run else []) + |
| (['--allow-dirty'] if (allow_dirty or self.allow_dirty) else []) + |
| (['--commit'] if commit else ['--no-commit']) + |
| (['--message', message] if message is not None else []) + |
| ['--list', part] |
| ) |
| log.debug("$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) |
| |
| with capture_logger("bumpversion.list") as out: |
| bumpversion.main(args) |
| |
| last_line = out.getvalue().splitlines()[-1] |
| new_version = last_line.replace("new_version=", "") |
| return new_version |
| |
| def format_changelog(self, version): |
| """ Write new header at beginning of changelog file with the specified |
| 'version' and the current date. |
| Return the changelog content for the current release. |
| """ |
| from datetime import datetime |
| |
| log.info("formatting changelog") |
| |
| changes = [] |
| with io.open(self.changelog_name, "r+", encoding="utf-8") as f: |
| for ln in f: |
| if self.version_RE.match(ln): |
| break |
| else: |
| changes.append(ln) |
| if not self.dry_run: |
| f.seek(0) |
| content = f.read() |
| date = datetime.today().strftime(self.date_fmt) |
| f.seek(0) |
| header = self.header_fmt % (version, date) |
| f.write(header + u"\n" + u"-"*len(header) + u"\n\n" + content) |
| |
| return u"".join(changes) |
| |
| |
| def find_data_files(manpath="share/man"): |
| """ Find FontTools's data_files (just man pages at this point). |
| |
| By default, we install man pages to "share/man" directory relative to the |
| base installation directory for data_files. The latter can be changed with |
| the --install-data option of 'setup.py install' sub-command. |
| |
| E.g., if the data files installation directory is "/usr", the default man |
| page installation directory will be "/usr/share/man". |
| |
| You can override this via the $FONTTOOLS_MANPATH environment variable. |
| |
| E.g., on some BSD systems man pages are installed to 'man' instead of |
| 'share/man'; you can export $FONTTOOLS_MANPATH variable just before |
| installing: |
| |
| $ FONTTOOLS_MANPATH="man" pip install -v . |
| [...] |
| running install_data |
| copying Doc/man/ttx.1 -> /usr/man/man1 |
| |
| When installing from PyPI, for this variable to have effect you need to |
| force pip to install from the source distribution instead of the wheel |
| package (otherwise setup.py is not run), by using the --no-binary option: |
| |
| $ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools |
| |
| Note that you can only override the base man path, i.e. without the |
| section number (man1, man3, etc.). The latter is always implied to be 1, |
| for "general commands". |
| """ |
| |
| # get base installation directory for man pages |
| manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath)) |
| # all our man pages go to section 1 |
| manpagedir = pjoin(manpagebase, 'man1') |
| |
| manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)] |
| |
| data_files = [(manpagedir, manpages)] |
| return data_files |
| |
| |
| setup( |
| name="fonttools", |
| version="3.43.2.dev0", |
| description="Tools to manipulate font files", |
| author="Just van Rossum", |
| author_email="just@letterror.com", |
| maintainer="Behdad Esfahbod", |
| maintainer_email="behdad@behdad.org", |
| url="http://github.com/fonttools/fonttools", |
| license="MIT", |
| platforms=["Any"], |
| long_description=long_description, |
| package_dir={'': 'Lib'}, |
| packages=find_packages("Lib"), |
| include_package_data=True, |
| data_files=find_data_files(), |
| setup_requires=wheel + bumpversion, |
| extras_require=extras_require, |
| entry_points={ |
| 'console_scripts': [ |
| "fonttools = fontTools.__main__:main", |
| "ttx = fontTools.ttx:main", |
| "pyftsubset = fontTools.subset:main", |
| "pyftmerge = fontTools.merge:main", |
| ] |
| }, |
| cmdclass={ |
| "release": release, |
| }, |
| **classifiers |
| ) |