Merge branch 'release-0.11.0'
[tor-bridgedb.git] / setup.py
blob74b6254393487cd123776a7c7faefdab98051352
1 #!/usr/bin/env python3
2 #_____________________________________________________________________________
4 # This file is part of BridgeDB, a Tor bridge distribution system.
6 # :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis@torproject.org>
7 # Aaron Gibson 0x2C4B239DD876C9F6 <aagbsn@torproject.org>
8 # Nick Mathewson 0x21194EBB165733EA <nickm@torproject.org>
9 # please also see AUTHORS file
10 # :copyright: (c) 2007-2013, The Tor Project, Inc.
11 # (c) 2007-2013, all entities within the AUTHORS file
12 # :license: see LICENSE for licensing information
13 #_____________________________________________________________________________
15 from __future__ import print_function
17 import os
18 import setuptools
19 import sys
21 from glob import glob
23 # Fix circular dependency with setup.py install
24 try:
25 from babel.messages.frontend import compile_catalog, extract_messages
26 from babel.messages.frontend import init_catalog, update_catalog
27 except ImportError:
28 compile_catalog = extract_messages = init_catalog = update_catalog = None
30 import versioneer
33 pkgpath = 'bridgedb'
35 # Repo directory that contains translations; this directory should contain
36 # both uncompiled translations (.po files) as well as compiled ones (.mo
37 # files). We only want to install the .mo files.
38 repo_i18n = os.path.join(pkgpath, 'i18n')
40 # The list of country codes for supported languages will be stored as a list
41 # variable, ``_supported``, in this file, so that the bridgedb packages
42 # __init__.py can access it:
43 repo_langs = os.path.join(pkgpath, '_langs.py')
45 # The directory containing template files and other resources to serve on the
46 # web server:
47 repo_templates = os.path.join(pkgpath, 'distributors', 'https', 'templates')
49 # The directories to install non-sourcecode resources into should always be
50 # given as relative paths, in order to force distutils to install relative to
51 # the rest of the codebase.
53 # Directory to installed compiled translations (.mo files) into:
54 install_i18n = os.path.join('bridgedb', 'i18n')
56 # Directory to install docs, license, and other text resources into:
57 install_docs = os.path.join('share', 'doc', 'bridgedb')
60 def get_cmdclass():
61 """Get our cmdclass dictionary for use in setuptool.setup().
63 This must be done outside the call to setuptools.setup() because we need
64 to add our own classes to the cmdclass dictionary, and then update that
65 dictionary with the one returned from versioneer.get_cmdclass().
66 """
67 cmdclass = {'test': Trial,
68 'compile_catalog': compile_catalog,
69 'extract_messages': extract_messages,
70 'init_catalog': init_catalog,
71 'update_catalog': update_catalog}
72 cmdclass.update(versioneer.get_cmdclass())
73 return cmdclass
75 def get_requirements():
76 """Extract the list of requirements from our requirements.txt.
78 :rtype: 2-tuple
79 :returns: Two lists, the first is a list of requirements in the form of
80 pkgname==version. The second is a list of URIs or VCS checkout strings
81 which specify the dependency links for obtaining a copy of the
82 requirement.
83 """
84 requirements_file = os.path.join(os.getcwd(), 'requirements.txt')
85 requirements = []
86 links=[]
87 try:
88 with open(requirements_file) as reqfile:
89 for line in reqfile.readlines():
90 line = line.strip()
91 if line.startswith('#'):
92 continue
93 if line.startswith(('git+', 'hg+', 'svn+')):
94 line = line[line.index('+') + 1:]
95 if line.startswith(
96 ('https://', 'git://', 'hg://', 'svn://')):
97 links.append(line)
98 else:
99 requirements.append(line)
101 except (IOError, OSError) as error:
102 print(error)
104 return requirements, links
106 def get_supported_langs():
107 """Get the paths for all compiled translation files.
109 The two-letter country code of each language which is going to be
110 installed will be added to a list, and this list will be written to
111 :attr:`repo_langs`, so that bridgedb/__init__.py can store a
112 package-level attribute ``bridgedb.__langs__``, which will be a list of
113 any languages which were installed.
115 Then, the paths of the compiled translations files are added to
116 :ivar:`data_files`. These should be included in the ``data_files``
117 parameter in :func:`~setuptools.setup` in order for setuptools to be able
118 to tell the underlying distutils ``install_data`` command to include these
119 files.
121 See http://docs.python.org/2/distutils/setupscript.html#installing-additional-files
122 for more information.
124 :ivar list supported: A list of two-letter country codes, one for each
125 language we currently provide translations support for.
126 :ivar list lang_dirs: The directories (relative or absolute) to install
127 the compiled translation file to.
128 :ivar list lang_files: The paths to compiled translations files, relative
129 to this setup.py script.
130 :rtype: list
131 :returns: Two lists, ``lang_dirs`` and ``lang_files``.
133 supported = []
134 lang_dirs = []
135 lang_files = []
137 for lang in os.listdir(repo_i18n):
138 if lang.endswith('templates'):
139 continue
140 supported.append(lang)
141 lang_dirs.append(os.path.join(install_i18n, lang))
142 lang_files.append(os.path.join(repo_i18n, lang,
143 'LC_MESSAGES', 'bridgedb.mo'))
144 supported.sort()
146 # Write our list of supported languages to 'bridgedb/_langs.py':
147 new_langs_lines = []
148 with open(repo_langs, 'r') as langsfile:
149 for line in langsfile.readlines():
150 if line.startswith('supported'):
151 # Change the 'supported' list() into a set():
152 line = "supported = set(%s)\n" % supported
153 new_langs_lines.append(line)
154 with open(repo_langs, 'w') as newlangsfile:
155 for line in new_langs_lines:
156 newlangsfile.write(line)
158 return lang_dirs, lang_files
160 def get_template_files():
161 """Return the paths to any web resource files to include in the package.
163 :rtype: list
164 :returns: Any files in :attr:`repo_templates` which match one of the glob
165 patterns in :ivar:`include_patterns`.
167 include_patterns = ['*.html',
168 '*.txt',
169 '*.asc',
170 'assets/*.png',
171 'assets/*.svg',
172 'assets/css/*.css',
173 'assets/font/*.woff',
174 'assets/font/*.ttf',
175 'assets/font/*.svg',
176 'assets/font/*.eot',
177 'assets/js/*.js',
178 'assets/images/*.svg',
179 'assets/images/*.ico']
180 template_files = []
182 for include_pattern in include_patterns:
183 pattern = os.path.join(repo_templates, include_pattern)
184 matches = glob(pattern)
185 template_files.extend(matches)
187 return template_files
189 def get_data_files(filesonly=False):
190 """Return any hard-coded data_files which should be distributed.
192 This is necessary so that both the distutils-derived :class:`installData`
193 class and the setuptools ``data_files`` parameter include the same files.
194 Call this function with ``filesonly=True`` to get a list of files suitable
195 for giving to the ``package_data`` parameter in ``setuptools.setup()``.
196 Or, call it with ``filesonly=False`` (the default) to get a list which is
197 suitable for using as ``distutils.command.install_data.data_files``.
199 :param bool filesonly: If true, only return the locations of the files to
200 install, not the directories to install them into.
201 :rtype: list
202 :returns: If ``filesonly``, returns a list of file paths. Otherwise,
203 returns a list of 2-tuples containing: one, the directory to install
204 to, and two, the files to install to that directory.
206 data_files = []
207 doc_files = ['README', 'TODO', 'LICENSE', 'requirements.txt']
208 lang_dirs, lang_files = get_supported_langs()
209 template_files = get_template_files()
211 if filesonly:
212 data_files.extend(doc_files)
213 for lst in lang_files, template_files:
214 for filename in lst:
215 if filename.startswith(pkgpath):
216 # The +1 gets rid of the '/' at the beginning:
217 filename = filename[len(pkgpath) + 1:]
218 data_files.append(filename)
219 else:
220 data_files.append((install_docs, doc_files))
221 for ldir, lfile in zip(lang_dirs, lang_files):
222 data_files.append((ldir, [lfile,]))
224 #[sys.stdout.write("Added data_file '%s'\n" % x) for x in data_files]
226 return data_files
229 class Trial(setuptools.Command):
230 """Twisted Trial setuptools command.
232 Based on the setuptools Trial command in Zooko's Tahoe-LAFS, as well as
233 https://github.com/simplegeo/setuptools-trial/ (which is also based on the
234 Tahoe-LAFS code).
236 Pieces of the original implementation of this 'test' command (that is, for
237 the original pyunit-based BridgeDB tests which, a long time ago, in a
238 galaxy far far away, lived in bridgedb.Tests) were based on setup.py from
239 Nick Mathewson's mixminion, which was based on the setup.py from Zooko's
240 pyutil package, which was in turn based on
241 http://mail.python.org/pipermail/distutils-sig/2002-January/002714.html.
243 Crusty, old-ass Python, like hella wut.
245 description = "Run Twisted Trial-based tests."
246 user_options = [
247 ('debug', 'b', ("Run tests in a debugger. If that debugger is pdb, will "
248 "load '.pdbrc' from current directory if it exists.")),
249 ('debug-stacktraces', 'B', "Report Deferred creation and callback stack traces"),
250 ('debugger=', None, ("The fully qualified name of a debugger to use if "
251 "--debug is passed (default: pdb)")),
252 ('disablegc', None, "Disable the garbage collector"),
253 ('force-gc', None, "Have Trial run gc.collect() before and after each test case"),
254 ('jobs=', 'j', "Number of local workers to run, a strictly positive integer"),
255 ('profile', None, "Run tests under the Python profiler"),
256 ('random=', 'Z', "Run tests in random order using the specified seed"),
257 ('reactor=', 'r', "Which reactor to use"),
258 ('reporter=', None, "Customize Trial's output with a reporter plugin"),
259 ('rterrors', 'e', "Realtime errors: print out tracebacks as soon as they occur"),
260 ('spew', None, "Print an insanely verbose log of everything that happens"),
261 ('testmodule=', None, "Filename to grep for test cases (-*- test-case-name)"),
262 ('tbformat=', None, ("Specify the format to display tracebacks with. Valid "
263 "formats are 'plain', 'emacs', and 'cgitb' which uses "
264 "the nicely verbose stdlib cgitb.text function")),
265 ('unclean-warnings', None, "Turn dirty reactor errors into warnings"),
266 ('until-failure', 'u', "Repeat a test (specified by -s) until it fails."),
267 ('without-module=', None, ("Fake the lack of the specified modules, separated "
268 "with commas")),
270 boolean_options = ['debug', 'debug-stacktraces', 'disablegc', 'force-gc',
271 'profile', 'rterrors', 'spew', 'unclean-warnings',
272 'until-failure']
274 def initialize_options(self):
275 self.debug = None
276 self.debug_stacktraces = None
277 self.debugger = None
278 self.disablegc = None
279 self.force_gc = None
280 self.jobs = None
281 self.profile = None
282 self.random = None
283 self.reactor = None
284 self.reporter = None
285 self.rterrors = None
286 self.spew = None
287 self.testmodule = None
288 self.tbformat = None
289 self.unclean_warnings = None
290 self.until_failure = None
291 self.without_module = None
293 def finalize_options(self):
294 build = self.get_finalized_command('build')
295 self.build_purelib = build.build_purelib
296 self.build_platlib = build.build_platlib
298 def run(self):
299 self.run_command('build')
300 old_path = sys.path[:]
301 sys.path[0:0] = [self.build_purelib, self.build_platlib]
303 result = 1
304 try:
305 result = self.run_tests()
306 finally:
307 sys.path = old_path
308 raise SystemExit(result)
310 def run_tests(self):
311 # We do the import from Twisted inside the function instead of the top
312 # of the file because since Twisted is a setup_requires, we can't
313 # assume that Twisted will be installed on the user's system prior, so
314 # if we don't do the import here, then importing from this plugin will
315 # fail.
316 from twisted.scripts import trial
318 if not self.testmodule:
319 self.testmodule = "bridgedb.test"
321 # Handle parsing the trial options passed through the setuptools
322 # trial command.
323 cmd_options = []
324 for opt in self.boolean_options:
325 if getattr(self, opt.replace('-', '_'), None):
326 cmd_options.append('--%s' % opt)
328 for opt in ('debugger', 'jobs', 'random', 'reactor', 'reporter',
329 'testmodule', 'tbformat', 'without-module'):
330 value = getattr(self, opt.replace('-', '_'), None)
331 if value is not None:
332 cmd_options.extend(['--%s' % opt, value])
334 config = trial.Options()
335 config.parseOptions(cmd_options)
336 config['tests'] = [self.testmodule,]
338 trial._initialDebugSetup(config)
339 trialRunner = trial._makeRunner(config)
340 suite = trial._getSuite(config)
342 # run the tests
343 if self.until_failure:
344 test_result = trialRunner.runUntilFailure(suite)
345 else:
346 test_result = trialRunner.run(suite)
348 if test_result.wasSuccessful():
349 return 0 # success
350 return 1 # failure
353 # If there is an environment variable BRIDGEDB_INSTALL_DEPENDENCIES=0, it will
354 # disable checking for, fetching, and installing BridgeDB's dependencies with
355 # easy_install.
357 # Setting BRIDGEDB_INSTALL_DEPENDENCIES=0 is *highly* recommended, because
358 # easy_install is a security nightmare. Automatically installing dependencies
359 # is enabled by default, however, because this is how all Python packages are
360 # supposed to work.
361 if bool(int(os.environ.get("BRIDGEDB_INSTALL_DEPENDENCIES", 1))):
362 requires, deplinks = get_requirements()
363 else:
364 requires, deplinks = [], []
367 setuptools.setup(
368 name='bridgedb',
369 version=versioneer.get_version(),
370 description='Backend systems for distribution of Tor bridge relays',
371 author='Nick Mathewson',
372 author_email='nickm at torproject dot org',
373 maintainer='Philipp Winter',
374 maintainer_email='phw@torproject.org',
375 url='https://www.torproject.org',
376 download_url='https://gitweb.torproject.org/bridgedb.git',
377 package_dir={'bridgedb': 'bridgedb'},
378 packages=['bridgedb',
379 'bridgedb.distributors',
380 'bridgedb.distributors.common',
381 'bridgedb.distributors.email',
382 'bridgedb.distributors.https',
383 'bridgedb.distributors.moat',
384 'bridgedb.parse',
385 'bridgedb.test',
387 scripts=['scripts/bridgedb',
388 'scripts/get-tor-exits'],
389 extras_require={'test': ["sure==1.2.2",
390 "coverage==4.2",
391 "cryptography==1.9"]},
392 zip_safe=False,
393 cmdclass=get_cmdclass(),
394 include_package_data=True,
395 install_requires=requires,
396 dependency_links=deplinks,
397 package_data={'bridgedb': get_data_files(filesonly=True)},
398 exclude_package_data={'bridgedb': ['*.po', '*.pot']},
399 message_extractors={
400 pkgpath: [
401 ('**.py', 'python', None),
402 ('distributors/https/templates/**.html', 'mako', None),