Skip to content

Commit 247b051

Browse files
committed
Merge https://github.com/pypa/distutils into feature/distutils-e1d5c9b1f6
2 parents 461f9a6 + e1d5c9b commit 247b051

File tree

3 files changed

+170
-108
lines changed

3 files changed

+170
-108
lines changed

setuptools/_distutils/command/build_scripts.py

+117-99
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
Implements the Distutils 'build_scripts' command."""
44

5-
import os, re
5+
import os
6+
import re
67
from stat import ST_MODE
78
from distutils import sysconfig
89
from distutils.core import Command
@@ -11,8 +12,14 @@
1112
from distutils import log
1213
import tokenize
1314

14-
# check if Python is called on the first line with this expression
15-
first_line_re = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
15+
shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$')
16+
"""
17+
Pattern matching a Python interpreter indicated in first line of a script.
18+
"""
19+
20+
# for Setuptools compatibility
21+
first_line_re = shebang_pattern
22+
1623

1724
class build_scripts(Command):
1825

@@ -26,13 +33,11 @@ class build_scripts(Command):
2633

2734
boolean_options = ['force']
2835

29-
3036
def initialize_options(self):
3137
self.build_dir = None
3238
self.scripts = None
3339
self.force = None
3440
self.executable = None
35-
self.outfiles = None
3641

3742
def finalize_options(self):
3843
self.set_undefined_options('build',
@@ -49,104 +54,117 @@ def run(self):
4954
return
5055
self.copy_scripts()
5156

52-
5357
def copy_scripts(self):
54-
r"""Copy each script listed in 'self.scripts'; if it's marked as a
55-
Python script in the Unix way (first line matches 'first_line_re',
56-
ie. starts with "\#!" and contains "python"), then adjust the first
57-
line to refer to the current Python interpreter as we copy.
58+
"""
59+
Copy each script listed in ``self.scripts``.
60+
61+
If a script is marked as a Python script (first line matches
62+
'shebang_pattern', i.e. starts with ``#!`` and contains
63+
"python"), then adjust in the copy the first line to refer to
64+
the current Python interpreter.
5865
"""
5966
self.mkpath(self.build_dir)
6067
outfiles = []
6168
updated_files = []
6269
for script in self.scripts:
63-
adjust = False
64-
script = convert_path(script)
65-
outfile = os.path.join(self.build_dir, os.path.basename(script))
66-
outfiles.append(outfile)
67-
68-
if not self.force and not newer(script, outfile):
69-
log.debug("not copying %s (up-to-date)", script)
70-
continue
71-
72-
# Always open the file, but ignore failures in dry-run mode --
73-
# that way, we'll get accurate feedback if we can read the
74-
# script.
75-
try:
76-
f = open(script, "rb")
77-
except OSError:
78-
if not self.dry_run:
79-
raise
80-
f = None
81-
else:
82-
encoding, lines = tokenize.detect_encoding(f.readline)
83-
f.seek(0)
84-
first_line = f.readline()
85-
if not first_line:
86-
self.warn("%s is an empty file (skipping)" % script)
87-
continue
88-
89-
match = first_line_re.match(first_line)
90-
if match:
91-
adjust = True
92-
post_interp = match.group(1) or b''
93-
94-
if adjust:
95-
log.info("copying and adjusting %s -> %s", script,
96-
self.build_dir)
97-
updated_files.append(outfile)
98-
if not self.dry_run:
99-
if not sysconfig.python_build:
100-
executable = self.executable
101-
else:
102-
executable = os.path.join(
103-
sysconfig.get_config_var("BINDIR"),
104-
"python%s%s" % (sysconfig.get_config_var("VERSION"),
105-
sysconfig.get_config_var("EXE")))
106-
executable = os.fsencode(executable)
107-
shebang = b"#!" + executable + post_interp + b"\n"
108-
# Python parser starts to read a script using UTF-8 until
109-
# it gets a #coding:xxx cookie. The shebang has to be the
110-
# first line of a file, the #coding:xxx cookie cannot be
111-
# written before. So the shebang has to be decodable from
112-
# UTF-8.
113-
try:
114-
shebang.decode('utf-8')
115-
except UnicodeDecodeError:
116-
raise ValueError(
117-
"The shebang ({!r}) is not decodable "
118-
"from utf-8".format(shebang))
119-
# If the script is encoded to a custom encoding (use a
120-
# #coding:xxx cookie), the shebang has to be decodable from
121-
# the script encoding too.
122-
try:
123-
shebang.decode(encoding)
124-
except UnicodeDecodeError:
125-
raise ValueError(
126-
"The shebang ({!r}) is not decodable "
127-
"from the script encoding ({})"
128-
.format(shebang, encoding))
129-
with open(outfile, "wb") as outf:
130-
outf.write(shebang)
131-
outf.writelines(f.readlines())
132-
if f:
133-
f.close()
134-
else:
135-
if f:
136-
f.close()
137-
updated_files.append(outfile)
138-
self.copy_file(script, outfile)
139-
140-
if os.name == 'posix':
141-
for file in outfiles:
142-
if self.dry_run:
143-
log.info("changing mode of %s", file)
144-
else:
145-
oldmode = os.stat(file)[ST_MODE] & 0o7777
146-
newmode = (oldmode | 0o555) & 0o7777
147-
if newmode != oldmode:
148-
log.info("changing mode of %s from %o to %o",
149-
file, oldmode, newmode)
150-
os.chmod(file, newmode)
151-
# XXX should we modify self.outfiles?
70+
self._copy_script(script, outfiles, updated_files)
71+
72+
self._change_modes(outfiles)
73+
15274
return outfiles, updated_files
75+
76+
def _copy_script(self, script, outfiles, updated_files):
77+
shebang_match = None
78+
script = convert_path(script)
79+
outfile = os.path.join(self.build_dir, os.path.basename(script))
80+
outfiles.append(outfile)
81+
82+
if not self.force and not newer(script, outfile):
83+
log.debug("not copying %s (up-to-date)", script)
84+
return
85+
86+
# Always open the file, but ignore failures in dry-run mode
87+
# in order to attempt to copy directly.
88+
try:
89+
f = tokenize.open(script)
90+
except OSError:
91+
if not self.dry_run:
92+
raise
93+
f = None
94+
else:
95+
first_line = f.readline()
96+
if not first_line:
97+
self.warn("%s is an empty file (skipping)" % script)
98+
return
99+
100+
shebang_match = shebang_pattern.match(first_line)
101+
102+
updated_files.append(outfile)
103+
if shebang_match:
104+
log.info("copying and adjusting %s -> %s", script,
105+
self.build_dir)
106+
if not self.dry_run:
107+
if not sysconfig.python_build:
108+
executable = self.executable
109+
else:
110+
executable = os.path.join(
111+
sysconfig.get_config_var("BINDIR"),
112+
"python%s%s" % (
113+
sysconfig.get_config_var("VERSION"),
114+
sysconfig.get_config_var("EXE")))
115+
post_interp = shebang_match.group(1) or ''
116+
shebang = "#!" + executable + post_interp + "\n"
117+
self._validate_shebang(shebang, f.encoding)
118+
with open(outfile, "w", encoding=f.encoding) as outf:
119+
outf.write(shebang)
120+
outf.writelines(f.readlines())
121+
if f:
122+
f.close()
123+
else:
124+
if f:
125+
f.close()
126+
self.copy_file(script, outfile)
127+
128+
def _change_modes(self, outfiles):
129+
if os.name != 'posix':
130+
return
131+
132+
for file in outfiles:
133+
self._change_mode(file)
134+
135+
def _change_mode(self, file):
136+
if self.dry_run:
137+
log.info("changing mode of %s", file)
138+
return
139+
140+
oldmode = os.stat(file)[ST_MODE] & 0o7777
141+
newmode = (oldmode | 0o555) & 0o7777
142+
if newmode != oldmode:
143+
log.info("changing mode of %s from %o to %o",
144+
file, oldmode, newmode)
145+
os.chmod(file, newmode)
146+
147+
@staticmethod
148+
def _validate_shebang(shebang, encoding):
149+
# Python parser starts to read a script using UTF-8 until
150+
# it gets a #coding:xxx cookie. The shebang has to be the
151+
# first line of a file, the #coding:xxx cookie cannot be
152+
# written before. So the shebang has to be encodable to
153+
# UTF-8.
154+
try:
155+
shebang.encode('utf-8')
156+
except UnicodeEncodeError:
157+
raise ValueError(
158+
"The shebang ({!r}) is not encodable "
159+
"to utf-8".format(shebang))
160+
161+
# If the script is encoded to a custom encoding (use a
162+
# #coding:xxx cookie), the shebang has to be encodable to
163+
# the script encoding too.
164+
try:
165+
shebang.encode(encoding)
166+
except UnicodeEncodeError:
167+
raise ValueError(
168+
"The shebang ({!r}) is not encodable "
169+
"to the script encoding ({})"
170+
.format(shebang, encoding))

setuptools/_distutils/command/check.py

+31-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
Implements the Distutils 'check' command.
44
"""
5+
from email.utils import getaddresses
6+
57
from distutils.core import Command
68
from distutils.errors import DistutilsSetupError
79

@@ -96,19 +98,39 @@ def check_metadata(self):
9698

9799
if missing:
98100
self.warn("missing required meta-data: %s" % ', '.join(missing))
99-
if metadata.author:
100-
if not metadata.author_email:
101-
self.warn("missing meta-data: if 'author' supplied, " +
102-
"'author_email' should be supplied too")
103-
elif metadata.maintainer:
104-
if not metadata.maintainer_email:
105-
self.warn("missing meta-data: if 'maintainer' supplied, " +
106-
"'maintainer_email' should be supplied too")
107-
else:
101+
if not (
102+
self._check_contact("author", metadata) or
103+
self._check_contact("maintainer", metadata)
104+
):
108105
self.warn("missing meta-data: either (author and author_email) " +
109106
"or (maintainer and maintainer_email) " +
110107
"should be supplied")
111108

109+
def _check_contact(self, kind, metadata):
110+
"""
111+
Returns True if the contact's name is specified and False otherwise.
112+
This function will warn if the contact's email is not specified.
113+
"""
114+
name = getattr(metadata, kind) or ''
115+
email = getattr(metadata, kind + '_email') or ''
116+
117+
msg = ("missing meta-data: if '{}' supplied, " +
118+
"'{}' should be supplied too")
119+
120+
if name and email:
121+
return True
122+
123+
if name:
124+
self.warn(msg.format(kind, kind + '_email'))
125+
return True
126+
127+
addresses = [(alias, addr) for alias, addr in getaddresses([email])]
128+
if any(alias and addr for alias, addr in addresses):
129+
# The contact's name can be encoded in the email: `Name <email>`
130+
return True
131+
132+
return False
133+
112134
def check_restructuredtext(self):
113135
"""Checks if the long string fields are reST-compliant."""
114136
data = self.distribution.get_long_description()

setuptools/_distutils/tests/test_check.py

+22
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ def test_check_metadata(self):
7171
cmd = self._run(metadata)
7272
self.assertEqual(cmd._warnings, 0)
7373

74+
def test_check_author_maintainer(self):
75+
for kind in ("author", "maintainer"):
76+
# ensure no warning when author_email or maintainer_email is given
77+
# (the spec allows these fields to take the form "Name <email>")
78+
metadata = {'url': 'xxx',
79+
kind + '_email': 'Name <[email protected]>',
80+
'name': 'xxx', 'version': 'xxx'}
81+
cmd = self._run(metadata)
82+
self.assertEqual(cmd._warnings, 0)
83+
84+
# the check should warn if only email is given and it does not
85+
# contain the name
86+
metadata[kind + '_email'] = '[email protected]'
87+
cmd = self._run(metadata)
88+
self.assertEqual(cmd._warnings, 1)
89+
90+
# the check should warn if only the name is given
91+
metadata[kind] = "Name"
92+
del metadata[kind + '_email']
93+
cmd = self._run(metadata)
94+
self.assertEqual(cmd._warnings, 1)
95+
7496
@unittest.skipUnless(HAS_DOCUTILS, "won't test without docutils")
7597
def test_check_document(self):
7698
pkg_info, dist = self.create_dist()

0 commit comments

Comments
 (0)