Change Travis's Python version to 3.7.
[tor-bridgedb.git] / setup.py
blob55dea958ab2cca611d2af370637b28575583e1ef
1 #!/usr/bin/env python
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 template_files = []
181 for include_pattern in include_patterns:
182 pattern = os.path.join(repo_templates, include_pattern)
183 matches = glob(pattern)
184 template_files.extend(matches)
186 return template_files
188 def get_data_files(filesonly=False):
189 """Return any hard-coded data_files which should be distributed.
191 This is necessary so that both the distutils-derived :class:`installData`
192 class and the setuptools ``data_files`` parameter include the same files.
193 Call this function with ``filesonly=True`` to get a list of files suitable
194 for giving to the ``package_data`` parameter in ``setuptools.setup()``.
195 Or, call it with ``filesonly=False`` (the default) to get a list which is
196 suitable for using as ``distutils.command.install_data.data_files``.
198 :param bool filesonly: If true, only return the locations of the files to
199 install, not the directories to install them into.
200 :rtype: list
201 :returns: If ``filesonly``, returns a list of file paths. Otherwise,
202 returns a list of 2-tuples containing: one, the directory to install
203 to, and two, the files to install to that directory.
205 data_files = []
206 doc_files = ['README', 'TODO', 'LICENSE', 'requirements.txt']
207 lang_dirs, lang_files = get_supported_langs()
208 template_files = get_template_files()
210 if filesonly:
211 data_files.extend(doc_files)
212 for lst in lang_files, template_files:
213 for filename in lst:
214 if filename.startswith(pkgpath):
215 # The +1 gets rid of the '/' at the beginning:
216 filename = filename[len(pkgpath) + 1:]
217 data_files.append(filename)
218 else:
219 data_files.append((install_docs, doc_files))
220 for ldir, lfile in zip(lang_dirs, lang_files):
221 data_files.append((ldir, [lfile,]))
223 #[sys.stdout.write("Added data_file '%s'\n" % x) for x in data_files]
225 return data_files
228 class Trial(setuptools.Command):
229 """Twisted Trial setuptools command.
231 Based on the setuptools Trial command in Zooko's Tahoe-LAFS, as well as
232 https://github.com/simplegeo/setuptools-trial/ (which is also based on the
233 Tahoe-LAFS code).
235 Pieces of the original implementation of this 'test' command (that is, for
236 the original pyunit-based BridgeDB tests which, a long time ago, in a
237 galaxy far far away, lived in bridgedb.Tests) were based on setup.py from
238 Nick Mathewson's mixminion, which was based on the setup.py from Zooko's
239 pyutil package, which was in turn based on
240 http://mail.python.org/pipermail/distutils-sig/2002-January/002714.html.
242 Crusty, old-ass Python, like hella wut.
244 description = "Run Twisted Trial-based tests."
245 user_options = [
246 ('debug', 'b', ("Run tests in a debugger. If that debugger is pdb, will "
247 "load '.pdbrc' from current directory if it exists.")),
248 ('debug-stacktraces', 'B', "Report Deferred creation and callback stack traces"),
249 ('debugger=', None, ("The fully qualified name of a debugger to use if "
250 "--debug is passed (default: pdb)")),
251 ('disablegc', None, "Disable the garbage collector"),
252 ('force-gc', None, "Have Trial run gc.collect() before and after each test case"),
253 ('jobs=', 'j', "Number of local workers to run, a strictly positive integer"),
254 ('profile', None, "Run tests under the Python profiler"),
255 ('random=', 'Z', "Run tests in random order using the specified seed"),
256 ('reactor=', 'r', "Which reactor to use"),
257 ('reporter=', None, "Customize Trial's output with a reporter plugin"),
258 ('rterrors', 'e', "Realtime errors: print out tracebacks as soon as they occur"),
259 ('spew', None, "Print an insanely verbose log of everything that happens"),
260 ('testmodule=', None, "Filename to grep for test cases (-*- test-case-name)"),
261 ('tbformat=', None, ("Specify the format to display tracebacks with. Valid "
262 "formats are 'plain', 'emacs', and 'cgitb' which uses "
263 "the nicely verbose stdlib cgitb.text function")),
264 ('unclean-warnings', None, "Turn dirty reactor errors into warnings"),
265 ('until-failure', 'u', "Repeat a test (specified by -s) until it fails."),
266 ('without-module=', None, ("Fake the lack of the specified modules, separated "
267 "with commas")),
269 boolean_options = ['debug', 'debug-stacktraces', 'disablegc', 'force-gc',
270 'profile', 'rterrors', 'spew', 'unclean-warnings',
271 'until-failure']
273 def initialize_options(self):
274 self.debug = None
275 self.debug_stacktraces = None
276 self.debugger = None
277 self.disablegc = None
278 self.force_gc = None
279 self.jobs = None
280 self.profile = None
281 self.random = None
282 self.reactor = None
283 self.reporter = None
284 self.rterrors = None
285 self.spew = None
286 self.testmodule = None
287 self.tbformat = None
288 self.unclean_warnings = None
289 self.until_failure = None
290 self.without_module = None
292 def finalize_options(self):
293 build = self.get_finalized_command('build')
294 self.build_purelib = build.build_purelib
295 self.build_platlib = build.build_platlib
297 def run(self):
298 self.run_command('build')
299 old_path = sys.path[:]
300 sys.path[0:0] = [self.build_purelib, self.build_platlib]
302 result = 1
303 try:
304 result = self.run_tests()
305 finally:
306 sys.path = old_path
307 raise SystemExit(result)
309 def run_tests(self):
310 # We do the import from Twisted inside the function instead of the top
311 # of the file because since Twisted is a setup_requires, we can't
312 # assume that Twisted will be installed on the user's system prior, so
313 # if we don't do the import here, then importing from this plugin will
314 # fail.
315 from twisted.scripts import trial
317 if not self.testmodule:
318 self.testmodule = "bridgedb.test"
320 # Handle parsing the trial options passed through the setuptools
321 # trial command.
322 cmd_options = []
323 for opt in self.boolean_options:
324 if getattr(self, opt.replace('-', '_'), None):
325 cmd_options.append('--%s' % opt)
327 for opt in ('debugger', 'jobs', 'random', 'reactor', 'reporter',
328 'testmodule', 'tbformat', 'without-module'):
329 value = getattr(self, opt.replace('-', '_'), None)
330 if value is not None:
331 cmd_options.extend(['--%s' % opt, value])
333 config = trial.Options()
334 config.parseOptions(cmd_options)
335 config['tests'] = [self.testmodule,]
337 trial._initialDebugSetup(config)
338 trialRunner = trial._makeRunner(config)
339 suite = trial._getSuite(config)
341 # run the tests
342 if self.until_failure:
343 test_result = trialRunner.runUntilFailure(suite)
344 else:
345 test_result = trialRunner.run(suite)
347 if test_result.wasSuccessful():
348 return 0 # success
349 return 1 # failure
352 # If there is an environment variable BRIDGEDB_INSTALL_DEPENDENCIES=0, it will
353 # disable checking for, fetching, and installing BridgeDB's dependencies with
354 # easy_install.
356 # Setting BRIDGEDB_INSTALL_DEPENDENCIES=0 is *highly* recommended, because
357 # easy_install is a security nightmare. Automatically installing dependencies
358 # is enabled by default, however, because this is how all Python packages are
359 # supposed to work.
360 if bool(int(os.environ.get("BRIDGEDB_INSTALL_DEPENDENCIES", 1))):
361 requires, deplinks = get_requirements()
362 else:
363 requires, deplinks = [], []
366 setuptools.setup(
367 name='bridgedb',
368 version=versioneer.get_version(),
369 description='Backend systems for distribution of Tor bridge relays',
370 author='Nick Mathewson',
371 author_email='nickm at torproject dot org',
372 maintainer='Philipp Winter',
373 maintainer_email='phw@torproject.org',
374 url='https://www.torproject.org',
375 download_url='https://gitweb.torproject.org/bridgedb.git',
376 package_dir={'bridgedb': 'bridgedb'},
377 packages=['bridgedb',
378 'bridgedb.distributors',
379 'bridgedb.distributors.common',
380 'bridgedb.distributors.email',
381 'bridgedb.distributors.https',
382 'bridgedb.distributors.moat',
383 'bridgedb.parse',
384 'bridgedb.test',
386 scripts=['scripts/bridgedb',
387 'scripts/get-tor-exits'],
388 extras_require={'test': ["sure==1.2.2",
389 "coverage==4.2",
390 "cryptography==1.9"]},
391 zip_safe=False,
392 cmdclass=get_cmdclass(),
393 include_package_data=True,
394 install_requires=requires,
395 dependency_links=deplinks,
396 package_data={'bridgedb': get_data_files(filesonly=True)},
397 exclude_package_data={'bridgedb': ['*.po', '*.pot']},
398 message_extractors={
399 pkgpath: [
400 ('**.py', 'python', None),
401 ('distributors/https/templates/**.html', 'mako', None),