3 Implements a Distutils 'upload_docs' subcommand (upload documentation to
4 sites other than PyPi such as devpi).
7 from base64
import standard_b64encode
8 from distutils
import log
9 from distutils
.errors
import DistutilsOptionError
21 from .._importlib
import metadata
22 from .. import SetuptoolsDeprecationWarning
24 from .upload
import upload
28 return s
.encode('utf-8', 'surrogateescape')
31 class upload_docs(upload
):
32 # override the default repository as upload_docs isn't
33 # supported by Warehouse (and won't be).
34 DEFAULT_REPOSITORY
= 'https://pypi.python.org/pypi/'
36 description
= 'Upload documentation to sites other than PyPi such as devpi'
40 "url of repository [default: %s]" % upload
.DEFAULT_REPOSITORY
),
41 ('show-response', None,
42 'display full response text from server'),
43 ('upload-dir=', None, 'directory to upload'),
45 boolean_options
= upload
.boolean_options
49 self
.upload_dir
is None
50 and metadata
.entry_points(group
='distutils.commands', name
='build_sphinx')
53 sub_commands
= [('build_sphinx', has_sphinx
)]
55 def initialize_options(self
):
56 upload
.initialize_options(self
)
57 self
.upload_dir
= None
58 self
.target_dir
= None
60 def finalize_options(self
):
62 "Upload_docs command is deprecated. Use Read the Docs "
63 "(https://readthedocs.org) instead.")
64 upload
.finalize_options(self
)
65 if self
.upload_dir
is None:
67 build_sphinx
= self
.get_finalized_command('build_sphinx')
68 self
.target_dir
= dict(build_sphinx
.builder_target_dirs
)['html']
70 build
= self
.get_finalized_command('build')
71 self
.target_dir
= os
.path
.join(build
.build_base
, 'docs')
73 self
.ensure_dirname('upload_dir')
74 self
.target_dir
= self
.upload_dir
75 self
.announce('Using upload directory %s' % self
.target_dir
)
77 def create_zipfile(self
, filename
):
78 zip_file
= zipfile
.ZipFile(filename
, "w")
80 self
.mkpath(self
.target_dir
) # just in case
81 for root
, dirs
, files
in os
.walk(self
.target_dir
):
82 if root
== self
.target_dir
and not files
:
83 tmpl
= "no files found in upload directory '%s'"
84 raise DistutilsOptionError(tmpl
% self
.target_dir
)
86 full
= os
.path
.join(root
, name
)
87 relative
= root
[len(self
.target_dir
):].lstrip(os
.path
.sep
)
88 dest
= os
.path
.join(relative
, name
)
89 zip_file
.write(full
, dest
)
95 "upload_docs is deprecated and will be removed in a future "
96 "version. Use tools like httpie or curl instead.",
97 SetuptoolsDeprecationWarning
,
101 for cmd_name
in self
.get_sub_commands():
102 self
.run_command(cmd_name
)
104 tmp_dir
= tempfile
.mkdtemp()
105 name
= self
.distribution
.metadata
.get_name()
106 zip_file
= os
.path
.join(tmp_dir
, "%s.zip" % name
)
108 self
.create_zipfile(zip_file
)
109 self
.upload_file(zip_file
)
111 shutil
.rmtree(tmp_dir
)
114 def _build_part(item
, sep_boundary
):
116 title
= '\nContent-Disposition: form-data; name="%s"' % key
117 # handle multiple entries for the same name
118 if not isinstance(values
, list):
121 if isinstance(value
, tuple):
122 title
+= '; filename="%s"' % value
[0]
125 value
= _encode(value
)
130 if value
and value
[-1:] == b
'\r':
131 yield b
'\n' # write an extra newline (lurve Macs)
134 def _build_multipart(cls
, data
):
136 Build up the MIME payload for the POST data
138 boundary
= '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
139 sep_boundary
= b
'\n--' + boundary
.encode('ascii')
140 end_boundary
= sep_boundary
+ b
'--'
141 end_items
= end_boundary
, b
"\n",
142 builder
= functools
.partial(
144 sep_boundary
=sep_boundary
,
146 part_groups
= map(builder
, data
.items())
147 parts
= itertools
.chain
.from_iterable(part_groups
)
148 body_items
= itertools
.chain(parts
, end_items
)
149 content_type
= 'multipart/form-data; boundary=%s' % boundary
150 return b
''.join(body_items
), content_type
152 def upload_file(self
, filename
):
153 with
open(filename
, 'rb') as f
:
155 meta
= self
.distribution
.metadata
157 ':action': 'doc_upload',
158 'name': meta
.get_name(),
159 'content': (os
.path
.basename(filename
), content
),
161 # set up the authentication
162 credentials
= _encode(self
.username
+ ':' + self
.password
)
163 credentials
= standard_b64encode(credentials
).decode('ascii')
164 auth
= "Basic " + credentials
166 body
, ct
= self
._build
_multipart
(data
)
168 msg
= "Submitting documentation to %s" % (self
.repository
)
169 self
.announce(msg
, log
.INFO
)
172 # We can't use urllib2 since we need to send the Basic
173 # auth right with the first request
174 schema
, netloc
, url
, params
, query
, fragments
= \
175 urllib
.parse
.urlparse(self
.repository
)
176 assert not params
and not query
and not fragments
178 conn
= http
.client
.HTTPConnection(netloc
)
179 elif schema
== 'https':
180 conn
= http
.client
.HTTPSConnection(netloc
)
182 raise AssertionError("unsupported schema " + schema
)
187 conn
.putrequest("POST", url
)
189 conn
.putheader('Content-type', content_type
)
190 conn
.putheader('Content-length', str(len(body
)))
191 conn
.putheader('Authorization', auth
)
194 except socket
.error
as e
:
195 self
.announce(str(e
), log
.ERROR
)
198 r
= conn
.getresponse()
200 msg
= 'Server response (%s): %s' % (r
.status
, r
.reason
)
201 self
.announce(msg
, log
.INFO
)
202 elif r
.status
== 301:
203 location
= r
.getheader('Location')
205 location
= 'https://pythonhosted.org/%s/' % meta
.get_name()
206 msg
= 'Upload successful. Visit %s' % location
207 self
.announce(msg
, log
.INFO
)
209 msg
= 'Upload failed (%s): %s' % (r
.status
, r
.reason
)
210 self
.announce(msg
, log
.ERROR
)
211 if self
.show_response
:
212 print('-' * 75, r
.read(), '-' * 75)