aboutsummaryrefslogtreecommitdiff
path: root/setup.py
blob: 6e6d39128f2b5b7474fdafea6606cf3fc827c7c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
#! /usr/bin/env python3

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, Extension
from setuptools.command.build_ext import build_ext as _build_ext
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

setup_requires = []

if {'bdist_wheel'}.intersection(sys.argv):
	setup_requires.append('wheel')

if {'release'}.intersection(sys.argv):
	setup_requires.append('bump2version')

try:
	__import__("cython")
except ImportError:
	has_cython = False
else:
	has_cython = True

env_with_cython = os.environ.get("FONTTOOLS_WITH_CYTHON")
with_cython = (
	True if env_with_cython in {"1", "true", "yes"}
	else False if env_with_cython in {"0", "false", "no"}
	else None
)
# --with-cython/--without-cython options override environment variables
opt_with_cython = {'--with-cython'}.intersection(sys.argv)
opt_without_cython = {'--without-cython'}.intersection(sys.argv)
if opt_with_cython and opt_without_cython:
	sys.exit(
		"error: the options '--with-cython' and '--without-cython' are "
		"mutually exclusive"
	)
elif opt_with_cython:
	sys.argv.remove("--with-cython")
	with_cython = True
elif opt_without_cython:
	sys.argv.remove("--without-cython")
	with_cython = False

if with_cython and not has_cython:
	setup_requires.append("cython")

ext_modules = []
if with_cython is True or (with_cython is None and has_cython):
	ext_modules.append(
		Extension("fontTools.cu2qu.cu2qu", ["Lib/fontTools/cu2qu/cu2qu.py"]),
	)

extras_require = {
	# for fontTools.ufoLib: to read/write UFO fonts
	"ufo": [
		"fs >= 2.2.0, < 3",
	],
	# 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",
	],
	# 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 == 'CPython'",
		"brotlicffi >= 0.8.0; platform_python_implementation != 'CPython'",
		"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": [
		# Python 3.11 already has Unicode 14.0, so the backport is not needed.
		(
			"unicodedata2 >= 14.0.0; python_version < '3.11'"
		),
	],
	# 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'",
	],
	# for fontTools.ttLib.removeOverlaps, to remove overlaps in TTF fonts
	"pathops": [
		"skia-pathops >= 0.5.0",
	],
	# for packing GSUB/GPOS tables with Harfbuzz repacker
	"repacker": [
		"uharfbuzz >= 0.23.0",
	],
}
# 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.cli

		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.cli.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


class cython_build_ext(_build_ext):
	"""Compile *.pyx source files to *.c using cythonize if Cython is
	installed and there is a working C compiler, else fall back to pure python dist.
	"""

	def finalize_options(self):
		from Cython.Build import cythonize

		# optionally enable line tracing for test coverage support
		linetrace = os.environ.get("CYTHON_TRACE") == "1"

		self.distribution.ext_modules[:] = cythonize(
			self.distribution.ext_modules,
			force=linetrace or self.force,
			annotate=os.environ.get("CYTHON_ANNOTATE") == "1",
			quiet=not self.verbose,
			compiler_directives={
				"linetrace": linetrace,
				"language_level": 3,
				"embedsignature": True,
			},
		)

		_build_ext.finalize_options(self)

	def build_extensions(self):
		try:
			_build_ext.build_extensions(self)
		except Exception as e:
			if with_cython:
				raise
			from distutils.errors import DistutilsModuleError

			# optional compilation failed: we delete 'ext_modules' and make sure
			# the generated wheel is 'pure'
			del self.distribution.ext_modules[:]
			try:
				bdist_wheel = self.get_finalized_command("bdist_wheel")
			except DistutilsModuleError:
				# 'bdist_wheel' command not available as wheel is not installed
				pass
			else:
				bdist_wheel.root_is_pure = True
			log.error('error: building extensions failed: %s' % e)

cmdclass = {"release": release}

if ext_modules:
    cmdclass["build_ext"] = cython_build_ext


setup_params = dict(
	name="fonttools",
	version="4.33.3",
	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"],
	python_requires=">=3.7",
	long_description=long_description,
	package_dir={'': 'Lib'},
	packages=find_packages("Lib"),
	include_package_data=True,
	data_files=find_data_files(),
	ext_modules=ext_modules,
	setup_requires=setup_requires,
	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=cmdclass,
	**classifiers
)


if __name__ == "__main__":
	setup(**setup_params)