1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Copyright 2005-2009,2011 Joe Wreschnig
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10import glob
11import os
12import shutil
13import sys
14import subprocess
15import tarfile
16
17from distutils.core import setup, Command, Distribution
18from distutils import dir_util
19
20
21def get_command_class(name):
22    # Returns the right class for either distutils or setuptools
23    return Distribution({}).get_command_class(name)
24
25
26distutils_clean = get_command_class("clean")
27
28
29class clean(distutils_clean):
30    def run(self):
31        # In addition to what the normal clean run does, remove pyc
32        # and pyo and backup files from the source tree.
33        distutils_clean.run(self)
34
35        def should_remove(filename):
36            if (filename.lower()[-4:] in [".pyc", ".pyo"] or
37                    filename.endswith("~") or
38                    (filename.startswith("#") and filename.endswith("#"))):
39                return True
40            else:
41                return False
42        for pathname, dirs, files in os.walk(os.path.dirname(__file__)):
43            for filename in filter(should_remove, files):
44                try:
45                    os.unlink(os.path.join(pathname, filename))
46                except EnvironmentError as err:
47                    print(str(err))
48
49        try:
50            os.unlink("MANIFEST")
51        except OSError:
52            pass
53
54        for base in ["coverage", "build", "dist"]:
55            path = os.path.join(os.path.dirname(__file__), base)
56            if os.path.isdir(path):
57                shutil.rmtree(path)
58
59
60distutils_sdist = get_command_class("sdist")
61
62
63class distcheck(distutils_sdist):
64
65    def _check_manifest(self):
66        assert self.get_archive_files()
67
68        # make sure MANIFEST.in includes all tracked files
69        if subprocess.call(["git", "status"],
70                           stdout=subprocess.PIPE,
71                           stderr=subprocess.PIPE) == 0:
72            # contains the packaged files after run() is finished
73            included_files = self.filelist.files
74            assert included_files
75
76            process = subprocess.Popen(
77                ["git", "ls-tree", "-r", "HEAD", "--name-only"],
78                stdout=subprocess.PIPE, universal_newlines=True)
79            out, err = process.communicate()
80            assert process.returncode == 0
81
82            tracked_files = out.splitlines()
83            for ignore in [".travis.yml", ".gitignore", ".codecov.yml",
84                           "azure-pipelines.yml"]:
85                tracked_files.remove(ignore)
86
87            diff = set(tracked_files) - set(included_files)
88            assert not diff, (
89                "Not all tracked files included in tarball, check MANIFEST.in",
90                diff)
91
92    def _check_dist(self):
93        assert self.get_archive_files()
94
95        distcheck_dir = os.path.join(self.dist_dir, "distcheck")
96        if os.path.exists(distcheck_dir):
97            dir_util.remove_tree(distcheck_dir)
98        self.mkpath(distcheck_dir)
99
100        archive = self.get_archive_files()[0]
101        tfile = tarfile.open(archive, "r:gz")
102        tfile.extractall(distcheck_dir)
103        tfile.close()
104
105        name = self.distribution.get_fullname()
106        extract_dir = os.path.join(distcheck_dir, name)
107
108        old_pwd = os.getcwd()
109        os.chdir(extract_dir)
110        self.spawn([sys.executable, "setup.py", "test"])
111        self.spawn([sys.executable, "setup.py", "build"])
112        self.spawn([sys.executable, "setup.py", "build_sphinx"])
113        self.spawn([sys.executable, "setup.py", "install",
114                    "--root", "../prefix", "--record", "../log.txt"])
115        os.chdir(old_pwd)
116
117    def run(self):
118        distutils_sdist.run(self)
119        self._check_manifest()
120        self._check_dist()
121
122
123class build_sphinx(Command):
124    description = "build sphinx documentation"
125    user_options = [
126        ("build-dir=", "d", "build directory"),
127    ]
128
129    def initialize_options(self):
130        self.build_dir = None
131
132    def finalize_options(self):
133        self.build_dir = self.build_dir or "build"
134
135    def run(self):
136        docs = "docs"
137        target = os.path.join(self.build_dir, "sphinx")
138        self.spawn(["sphinx-build", "-b", "html", "-n", docs, target])
139
140
141class test_cmd(Command):
142    description = "run automated tests"
143    user_options = [
144        ("to-run=", None, "list of tests to run (default all)"),
145        ("exitfirst", "x", "stop after first failing test"),
146        ("no-quality", None, "skip code quality tests"),
147    ]
148
149    def initialize_options(self):
150        self.to_run = []
151        self.exitfirst = False
152        self.no_quality = False
153
154    def finalize_options(self):
155        if self.to_run:
156            self.to_run = self.to_run.split(",")
157        self.exitfirst = bool(self.exitfirst)
158        self.no_quality = bool(self.no_quality)
159
160    def run(self):
161        import tests
162
163        status = tests.unit(self.to_run, self.exitfirst, self.no_quality)
164        if status != 0:
165            raise SystemExit(status)
166
167
168class quality_cmd(Command):
169    description = "run pyflakes/pep8 tests"
170    user_options = []
171
172    def initialize_options(self):
173        pass
174
175    def finalize_options(self):
176        pass
177
178    def run(self):
179        import tests
180
181        status = tests.check()
182        if status != 0:
183            raise SystemExit(status)
184
185
186class coverage_cmd(Command):
187    description = "generate test coverage data"
188    user_options = []
189
190    def initialize_options(self):
191        pass
192
193    def finalize_options(self):
194        pass
195
196    def run(self):
197        try:
198            from coverage import coverage
199        except ImportError:
200            raise SystemExit(
201                "Missing 'coverage' module. See "
202                "https://pypi.python.org/pypi/coverage or try "
203                "`apt-get install python-coverage python3-coverage`")
204
205        for key in list(sys.modules.keys()):
206            if key.startswith('mutagen'):
207                del(sys.modules[key])
208
209        cov = coverage()
210        cov.start()
211
212        cmd = self.reinitialize_command("test")
213        cmd.ensure_finalized()
214        cmd.run()
215
216        dest = os.path.join(os.getcwd(), "coverage")
217
218        cov.stop()
219        cov.html_report(
220            directory=dest,
221            ignore_errors=True,
222            include=["mutagen/*"],
223            omit=["mutagen/_senf/*"])
224
225        print("Coverage summary: file://%s/index.html" % dest)
226
227
228if __name__ == "__main__":
229    from mutagen import version
230
231    with open('README.rst') as h:
232        long_description = h.read()
233
234    # convert to a setuptools compatible version string
235    if version[-1] == -1:
236        version_string = ".".join(map(str, version[:-1])) + ".dev0"
237    else:
238        version_string = ".".join(map(str, version))
239
240    if os.name == "posix":
241        data_files = [('share/man/man1', glob.glob("man/*.1"))]
242    else:
243        data_files = []
244
245    cmd_classes = {
246        "clean": clean,
247        "test": test_cmd,
248        "quality": quality_cmd,
249        "coverage": coverage_cmd,
250        "distcheck": distcheck,
251        "build_sphinx": build_sphinx,
252    }
253
254    setup(cmdclass=cmd_classes,
255          name="mutagen",
256          version=version_string,
257          url="https://github.com/quodlibet/mutagen",
258          description="read and write audio tags for many formats",
259          author="Michael Urman",
260          author_email="quod-libet-development@groups.google.com",
261          license="GPL-2.0-or-later",
262          classifiers=[
263            'Operating System :: OS Independent',
264            'Programming Language :: Python :: 2',
265            'Programming Language :: Python :: 2.7',
266            'Programming Language :: Python :: 3',
267            'Programming Language :: Python :: 3.4',
268            'Programming Language :: Python :: 3.5',
269            'Programming Language :: Python :: 3.6',
270            'Programming Language :: Python :: 3.7',
271            'Programming Language :: Python :: Implementation :: CPython',
272            'Programming Language :: Python :: Implementation :: PyPy',
273            ('License :: OSI Approved :: '
274             'GNU General Public License v2 or later (GPLv2+)'),
275            'Topic :: Multimedia :: Sound/Audio',
276          ],
277          packages=[
278            "mutagen",
279            "mutagen.id3",
280            "mutagen.mp4",
281            "mutagen.asf",
282            "mutagen.mp3",
283            "mutagen._senf",
284            "mutagen._tools",
285          ],
286          data_files=data_files,
287          scripts=[os.path.join("tools", name) for name in [
288            "mid3cp",
289            "mid3iconv",
290            "mid3v2",
291            "moggsplit",
292            "mutagen-inspect",
293            "mutagen-pony",
294          ]],
295          long_description=long_description,
296    )
297