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
20 from .._importlib
import metadata
21 from ..warnings
import SetuptoolsDeprecationWarning
23 from .upload
import upload
27 return s
.encode('utf-8', 'surrogateescape')
30 class upload_docs(upload
):
31 # override the default repository as upload_docs isn't
32 # supported by Warehouse (and won't be).
33 DEFAULT_REPOSITORY
= 'https://pypi.python.org/pypi/'
35 description
= 'Upload documentation to sites other than PyPi such as devpi'
41 "url of repository [default: %s]" % upload
.DEFAULT_REPOSITORY
,
43 ('show-response', None, 'display full response text from server'),
44 ('upload-dir=', None, 'directory to upload'),
46 boolean_options
= upload
.boolean_options
50 self
.upload_dir
is None
51 and metadata
.entry_points(group
='distutils.commands', name
='build_sphinx')
54 sub_commands
= [('build_sphinx', has_sphinx
)]
56 def initialize_options(self
):
57 upload
.initialize_options(self
)
58 self
.upload_dir
= None
59 self
.target_dir
= None
61 def finalize_options(self
):
63 "Upload_docs command is deprecated. Use Read the Docs "
64 "(https://readthedocs.org) instead."
66 upload
.finalize_options(self
)
67 if self
.upload_dir
is None:
69 build_sphinx
= self
.get_finalized_command('build_sphinx')
70 self
.target_dir
= dict(build_sphinx
.builder_target_dirs
)['html']
72 build
= self
.get_finalized_command('build')
73 self
.target_dir
= os
.path
.join(build
.build_base
, 'docs')
75 self
.ensure_dirname('upload_dir')
76 self
.target_dir
= self
.upload_dir
77 self
.announce('Using upload directory %s' % self
.target_dir
)
79 def create_zipfile(self
, filename
):
80 zip_file
= zipfile
.ZipFile(filename
, "w")
82 self
.mkpath(self
.target_dir
) # just in case
83 for root
, dirs
, files
in os
.walk(self
.target_dir
):
84 if root
== self
.target_dir
and not files
:
85 tmpl
= "no files found in upload directory '%s'"
86 raise DistutilsOptionError(tmpl
% self
.target_dir
)
88 full
= os
.path
.join(root
, name
)
89 relative
= root
[len(self
.target_dir
) :].lstrip(os
.path
.sep
)
90 dest
= os
.path
.join(relative
, name
)
91 zip_file
.write(full
, dest
)
96 SetuptoolsDeprecationWarning
.emit(
99 upload_docs is deprecated and will be removed in a future version.
100 Instead, use tools like devpi and Read the Docs; or lower level tools like
101 httpie and curl to interact directly with your hosting service API.
103 due_date
=(2023, 9, 26), # warning introduced in 27 Jul 2022
107 for cmd_name
in self
.get_sub_commands():
108 self
.run_command(cmd_name
)
110 tmp_dir
= tempfile
.mkdtemp()
111 name
= self
.distribution
.metadata
.get_name()
112 zip_file
= os
.path
.join(tmp_dir
, "%s.zip" % name
)
114 self
.create_zipfile(zip_file
)
115 self
.upload_file(zip_file
)
117 shutil
.rmtree(tmp_dir
)
120 def _build_part(item
, sep_boundary
):
122 title
= '\nContent-Disposition: form-data; name="%s"' % key
123 # handle multiple entries for the same name
124 if not isinstance(values
, list):
127 if isinstance(value
, tuple):
128 title
+= '; filename="%s"' % value
[0]
131 value
= _encode(value
)
136 if value
and value
[-1:] == b
'\r':
137 yield b
'\n' # write an extra newline (lurve Macs)
140 def _build_multipart(cls
, data
):
142 Build up the MIME payload for the POST data
144 boundary
= '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
145 sep_boundary
= b
'\n--' + boundary
.encode('ascii')
146 end_boundary
= sep_boundary
+ b
'--'
151 builder
= functools
.partial(
153 sep_boundary
=sep_boundary
,
155 part_groups
= map(builder
, data
.items())
156 parts
= itertools
.chain
.from_iterable(part_groups
)
157 body_items
= itertools
.chain(parts
, end_items
)
158 content_type
= 'multipart/form-data; boundary=%s' % boundary
159 return b
''.join(body_items
), content_type
161 def upload_file(self
, filename
):
162 with open(filename
, 'rb') as f
:
164 meta
= self
.distribution
.metadata
166 ':action': 'doc_upload',
167 'name': meta
.get_name(),
168 'content': (os
.path
.basename(filename
), content
),
170 # set up the authentication
171 credentials
= _encode(self
.username
+ ':' + self
.password
)
172 credentials
= standard_b64encode(credentials
).decode('ascii')
173 auth
= "Basic " + credentials
175 body
, ct
= self
._build
_multipart
(data
)
177 msg
= "Submitting documentation to %s" % (self
.repository
)
178 self
.announce(msg
, log
.INFO
)
181 # We can't use urllib2 since we need to send the Basic
182 # auth right with the first request
183 schema
, netloc
, url
, params
, query
, fragments
= urllib
.parse
.urlparse(
186 assert not params
and not query
and not fragments
188 conn
= http
.client
.HTTPConnection(netloc
)
189 elif schema
== 'https':
190 conn
= http
.client
.HTTPSConnection(netloc
)
192 raise AssertionError("unsupported schema " + schema
)
197 conn
.putrequest("POST", url
)
199 conn
.putheader('Content-type', content_type
)
200 conn
.putheader('Content-length', str(len(body
)))
201 conn
.putheader('Authorization', auth
)
204 except socket
.error
as e
:
205 self
.announce(str(e
), log
.ERROR
)
208 r
= conn
.getresponse()
210 msg
= 'Server response (%s): %s' % (r
.status
, r
.reason
)
211 self
.announce(msg
, log
.INFO
)
212 elif r
.status
== 301:
213 location
= r
.getheader('Location')
215 location
= 'https://pythonhosted.org/%s/' % meta
.get_name()
216 msg
= 'Upload successful. Visit %s' % location
217 self
.announce(msg
, log
.INFO
)
219 msg
= 'Upload failed (%s): %s' % (r
.status
, r
.reason
)
220 self
.announce(msg
, log
.ERROR
)
221 if self
.show_response
:
222 print('-' * 75, r
.read(), '-' * 75)