• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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