1#! /usr/bin/env python 2 3from __future__ import print_function 4import io 5import sys 6import os 7from os.path import isfile, join as pjoin 8from glob import glob 9from setuptools import setup, find_packages, Command 10from distutils import log 11from distutils.util import convert_path 12import subprocess as sp 13import contextlib 14import re 15 16# Force distutils to use py_compile.compile() function with 'doraise' argument 17# set to True, in order to raise an exception on compilation errors 18import py_compile 19orig_py_compile = py_compile.compile 20 21def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): 22 orig_py_compile(file, cfile=cfile, dfile=dfile, doraise=True) 23 24py_compile.compile = doraise_py_compile 25 26needs_wheel = {'bdist_wheel'}.intersection(sys.argv) 27wheel = ['wheel'] if needs_wheel else [] 28needs_bumpversion = {'release'}.intersection(sys.argv) 29bumpversion = ['bump2version'] if needs_bumpversion else [] 30 31extras_require = { 32 # for fontTools.ufoLib: to read/write UFO fonts 33 "ufo": [ 34 "fs >= 2.2.0, < 3", 35 "enum34 >= 1.1.6; python_version < '3.4'", 36 ], 37 # for fontTools.misc.etree and fontTools.misc.plistlib: use lxml to 38 # read/write XML files (faster/safer than built-in ElementTree) 39 "lxml": [ 40 "lxml >= 4.0, < 5", 41 "singledispatch >= 3.4.0.3; python_version < '3.4'", 42 # typing >= 3.6.4 is required when using ABC collections with the 43 # singledispatch backport, see: 44 # https://github.com/fonttools/fonttools/issues/1423 45 # https://github.com/python/typing/issues/484 46 "typing >= 3.6.4; python_version < '3.4'", 47 ], 48 # for fontTools.sfnt and fontTools.woff2: to compress/uncompress 49 # WOFF 1.0 and WOFF 2.0 webfonts. 50 "woff": [ 51 "brotli >= 1.0.1; platform_python_implementation != 'PyPy'", 52 "brotlipy >= 0.7.0; platform_python_implementation == 'PyPy'", 53 "zopfli >= 0.1.4", 54 ], 55 # for fontTools.unicode and fontTools.unicodedata: to use the latest version 56 # of the Unicode Character Database instead of the built-in unicodedata 57 # which varies between python versions and may be outdated. 58 "unicode": [ 59 # the unicodedata2 extension module doesn't work on PyPy. 60 # Python 3.7 already has Unicode 11, so the backport is not needed. 61 ( 62 "unicodedata2 >= 11.0.0; " 63 "python_version < '3.7' and platform_python_implementation != 'PyPy'" 64 ), 65 ], 66 # for graphite type tables in ttLib/tables (Silf, Glat, Gloc) 67 "graphite": [ 68 "lz4 >= 1.7.4.2" 69 ], 70 # for fontTools.interpolatable: to solve the "minimum weight perfect 71 # matching problem in bipartite graphs" (aka Assignment problem) 72 "interpolatable": [ 73 # use pure-python alternative on pypy 74 "scipy; platform_python_implementation != 'PyPy'", 75 "munkres; platform_python_implementation == 'PyPy'", 76 ], 77 # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting 78 # VariationModel 79 "plot": [ 80 # TODO: figure out the minimum version of matplotlib that we need 81 "matplotlib", 82 ], 83 # for fontTools.misc.symfont, module for symbolic font statistics analysis 84 "symfont": [ 85 "sympy", 86 ], 87 # To get file creator and type of Macintosh PostScript Type 1 fonts (macOS only) 88 "type1": [ 89 "xattr; sys_platform == 'darwin'", 90 ], 91} 92# use a special 'all' key as shorthand to includes all the extra dependencies 93extras_require["all"] = sum(extras_require.values(), []) 94 95 96# Trove classifiers for PyPI 97classifiers = {"classifiers": [ 98 "Development Status :: 5 - Production/Stable", 99 "Environment :: Console", 100 "Environment :: Other Environment", 101 "Intended Audience :: Developers", 102 "Intended Audience :: End Users/Desktop", 103 "License :: OSI Approved :: MIT License", 104 "Natural Language :: English", 105 "Operating System :: OS Independent", 106 "Programming Language :: Python", 107 "Programming Language :: Python :: 2", 108 "Programming Language :: Python :: 3", 109 "Topic :: Text Processing :: Fonts", 110 "Topic :: Multimedia :: Graphics", 111 "Topic :: Multimedia :: Graphics :: Graphics Conversion", 112]} 113 114 115# concatenate README.rst and NEWS.rest into long_description so they are 116# displayed on the FontTols project page on PyPI 117with io.open("README.rst", "r", encoding="utf-8") as readme: 118 long_description = readme.read() 119long_description += "\nChangelog\n~~~~~~~~~\n\n" 120with io.open("NEWS.rst", "r", encoding="utf-8") as changelog: 121 long_description += changelog.read() 122 123 124@contextlib.contextmanager 125def capture_logger(name): 126 """ Context manager to capture a logger output with a StringIO stream. 127 """ 128 import logging 129 130 logger = logging.getLogger(name) 131 try: 132 import StringIO 133 stream = StringIO.StringIO() 134 except ImportError: 135 stream = io.StringIO() 136 handler = logging.StreamHandler(stream) 137 logger.addHandler(handler) 138 try: 139 yield stream 140 finally: 141 logger.removeHandler(handler) 142 143 144class release(Command): 145 """ 146 Tag a new release with a single command, using the 'bumpversion' tool 147 to update all the version strings in the source code. 148 The version scheme conforms to 'SemVer' and PEP 440 specifications. 149 150 Firstly, the pre-release '.devN' suffix is dropped to signal that this is 151 a stable release. If '--major' or '--minor' options are passed, the 152 the first or second 'semver' digit is also incremented. Major is usually 153 for backward-incompatible API changes, while minor is used when adding 154 new backward-compatible functionalities. No options imply 'patch' or bug-fix 155 release. 156 157 A new header is also added to the changelog file ("NEWS.rst"), containing 158 the new version string and the current 'YYYY-MM-DD' date. 159 160 All changes are committed, and an annotated git tag is generated. With the 161 --sign option, the tag is GPG-signed with the user's default key. 162 163 Finally, the 'patch' part of the version string is bumped again, and a 164 pre-release suffix '.dev0' is appended to mark the opening of a new 165 development cycle. 166 167 Links: 168 - http://semver.org/ 169 - https://www.python.org/dev/peps/pep-0440/ 170 - https://github.com/c4urself/bump2version 171 """ 172 173 description = "update version strings for release" 174 175 user_options = [ 176 ("major", None, "bump the first digit (incompatible API changes)"), 177 ("minor", None, "bump the second digit (new backward-compatible features)"), 178 ("sign", "s", "make a GPG-signed tag, using the default key"), 179 ("allow-dirty", None, "don't abort if working directory is dirty"), 180 ] 181 182 changelog_name = "NEWS.rst" 183 version_RE = re.compile("^[0-9]+\.[0-9]+") 184 date_fmt = u"%Y-%m-%d" 185 header_fmt = u"%s (released %s)" 186 commit_message = "Release {new_version}" 187 tag_name = "{new_version}" 188 version_files = [ 189 "setup.cfg", 190 "setup.py", 191 "Lib/fontTools/__init__.py", 192 ] 193 194 def initialize_options(self): 195 self.minor = False 196 self.major = False 197 self.sign = False 198 self.allow_dirty = False 199 200 def finalize_options(self): 201 if all([self.major, self.minor]): 202 from distutils.errors import DistutilsOptionError 203 raise DistutilsOptionError("--major/--minor are mutually exclusive") 204 self.part = "major" if self.major else "minor" if self.minor else None 205 206 def run(self): 207 if self.part is not None: 208 log.info("bumping '%s' version" % self.part) 209 self.bumpversion(self.part, commit=False) 210 release_version = self.bumpversion( 211 "release", commit=False, allow_dirty=True) 212 else: 213 log.info("stripping pre-release suffix") 214 release_version = self.bumpversion("release") 215 log.info(" version = %s" % release_version) 216 217 changes = self.format_changelog(release_version) 218 219 self.git_commit(release_version) 220 self.git_tag(release_version, changes, self.sign) 221 222 log.info("bumping 'patch' version and pre-release suffix") 223 next_dev_version = self.bumpversion('patch', commit=True) 224 log.info(" version = %s" % next_dev_version) 225 226 def git_commit(self, version): 227 """ Stage and commit all relevant version files, and format the commit 228 message with specified 'version' string. 229 """ 230 files = self.version_files + [self.changelog_name] 231 232 log.info("committing changes") 233 for f in files: 234 log.info(" %s" % f) 235 if self.dry_run: 236 return 237 sp.check_call(["git", "add"] + files) 238 msg = self.commit_message.format(new_version=version) 239 sp.check_call(["git", "commit", "-m", msg], stdout=sp.PIPE) 240 241 def git_tag(self, version, message, sign=False): 242 """ Create annotated git tag with given 'version' and 'message'. 243 Optionally 'sign' the tag with the user's GPG key. 244 """ 245 log.info("creating %s git tag '%s'" % ( 246 "signed" if sign else "annotated", version)) 247 if self.dry_run: 248 return 249 # create an annotated (or signed) tag from the new version 250 tag_opt = "-s" if sign else "-a" 251 tag_name = self.tag_name.format(new_version=version) 252 proc = sp.Popen( 253 ["git", "tag", tag_opt, "-F", "-", tag_name], stdin=sp.PIPE) 254 # use the latest changes from the changelog file as the tag message 255 tag_message = u"%s\n\n%s" % (tag_name, message) 256 proc.communicate(tag_message.encode('utf-8')) 257 if proc.returncode != 0: 258 sys.exit(proc.returncode) 259 260 def bumpversion(self, part, commit=False, message=None, allow_dirty=None): 261 """ Run bumpversion.main() with the specified arguments, and return the 262 new computed version string (cf. 'bumpversion --help' for more info) 263 """ 264 import bumpversion 265 266 args = ( 267 (['--verbose'] if self.verbose > 1 else []) + 268 (['--dry-run'] if self.dry_run else []) + 269 (['--allow-dirty'] if (allow_dirty or self.allow_dirty) else []) + 270 (['--commit'] if commit else ['--no-commit']) + 271 (['--message', message] if message is not None else []) + 272 ['--list', part] 273 ) 274 log.debug("$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) 275 276 with capture_logger("bumpversion.list") as out: 277 bumpversion.main(args) 278 279 last_line = out.getvalue().splitlines()[-1] 280 new_version = last_line.replace("new_version=", "") 281 return new_version 282 283 def format_changelog(self, version): 284 """ Write new header at beginning of changelog file with the specified 285 'version' and the current date. 286 Return the changelog content for the current release. 287 """ 288 from datetime import datetime 289 290 log.info("formatting changelog") 291 292 changes = [] 293 with io.open(self.changelog_name, "r+", encoding="utf-8") as f: 294 for ln in f: 295 if self.version_RE.match(ln): 296 break 297 else: 298 changes.append(ln) 299 if not self.dry_run: 300 f.seek(0) 301 content = f.read() 302 date = datetime.today().strftime(self.date_fmt) 303 f.seek(0) 304 header = self.header_fmt % (version, date) 305 f.write(header + u"\n" + u"-"*len(header) + u"\n\n" + content) 306 307 return u"".join(changes) 308 309 310def find_data_files(manpath="share/man"): 311 """ Find FontTools's data_files (just man pages at this point). 312 313 By default, we install man pages to "share/man" directory relative to the 314 base installation directory for data_files. The latter can be changed with 315 the --install-data option of 'setup.py install' sub-command. 316 317 E.g., if the data files installation directory is "/usr", the default man 318 page installation directory will be "/usr/share/man". 319 320 You can override this via the $FONTTOOLS_MANPATH environment variable. 321 322 E.g., on some BSD systems man pages are installed to 'man' instead of 323 'share/man'; you can export $FONTTOOLS_MANPATH variable just before 324 installing: 325 326 $ FONTTOOLS_MANPATH="man" pip install -v . 327 [...] 328 running install_data 329 copying Doc/man/ttx.1 -> /usr/man/man1 330 331 When installing from PyPI, for this variable to have effect you need to 332 force pip to install from the source distribution instead of the wheel 333 package (otherwise setup.py is not run), by using the --no-binary option: 334 335 $ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools 336 337 Note that you can only override the base man path, i.e. without the 338 section number (man1, man3, etc.). The latter is always implied to be 1, 339 for "general commands". 340 """ 341 342 # get base installation directory for man pages 343 manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath)) 344 # all our man pages go to section 1 345 manpagedir = pjoin(manpagebase, 'man1') 346 347 manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)] 348 349 data_files = [(manpagedir, manpages)] 350 return data_files 351 352 353setup( 354 name="fonttools", 355 version="3.39.0", 356 description="Tools to manipulate font files", 357 author="Just van Rossum", 358 author_email="just@letterror.com", 359 maintainer="Behdad Esfahbod", 360 maintainer_email="behdad@behdad.org", 361 url="http://github.com/fonttools/fonttools", 362 license="MIT", 363 platforms=["Any"], 364 long_description=long_description, 365 package_dir={'': 'Lib'}, 366 packages=find_packages("Lib"), 367 include_package_data=True, 368 data_files=find_data_files(), 369 setup_requires=wheel + bumpversion, 370 extras_require=extras_require, 371 entry_points={ 372 'console_scripts': [ 373 "fonttools = fontTools.__main__:main", 374 "ttx = fontTools.ttx:main", 375 "pyftsubset = fontTools.subset:main", 376 "pyftmerge = fontTools.merge:main", 377 ] 378 }, 379 cmdclass={ 380 "release": release, 381 }, 382 **classifiers 383) 384