test_urllib.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. """Regresssion tests for urllib"""
  2. import collections
  3. import urllib
  4. import httplib
  5. import io
  6. import unittest
  7. import os
  8. import sys
  9. import mimetools
  10. import tempfile
  11. from test import test_support
  12. from base64 import b64encode
  13. def hexescape(char):
  14. """Escape char as RFC 2396 specifies"""
  15. hex_repr = hex(ord(char))[2:].upper()
  16. if len(hex_repr) == 1:
  17. hex_repr = "0%s" % hex_repr
  18. return "%" + hex_repr
  19. def fakehttp(fakedata):
  20. class FakeSocket(io.BytesIO):
  21. def sendall(self, data):
  22. FakeHTTPConnection.buf = data
  23. def makefile(self, *args, **kwds):
  24. return self
  25. def read(self, amt=None):
  26. if self.closed:
  27. return b""
  28. return io.BytesIO.read(self, amt)
  29. def readline(self, length=None):
  30. if self.closed:
  31. return b""
  32. return io.BytesIO.readline(self, length)
  33. class FakeHTTPConnection(httplib.HTTPConnection):
  34. # buffer to store data for verification in urlopen tests.
  35. buf = ""
  36. def connect(self):
  37. self.sock = FakeSocket(self.fakedata)
  38. self.__class__.fakesock = self.sock
  39. FakeHTTPConnection.fakedata = fakedata
  40. return FakeHTTPConnection
  41. class FakeHTTPMixin(object):
  42. def fakehttp(self, fakedata):
  43. assert httplib.HTTP._connection_class == httplib.HTTPConnection
  44. httplib.HTTP._connection_class = fakehttp(fakedata)
  45. def unfakehttp(self):
  46. httplib.HTTP._connection_class = httplib.HTTPConnection
  47. class urlopen_FileTests(unittest.TestCase):
  48. """Test urlopen() opening a temporary file.
  49. Try to test as much functionality as possible so as to cut down on reliance
  50. on connecting to the Net for testing.
  51. """
  52. def setUp(self):
  53. """Setup of a temp file to use for testing"""
  54. self.text = "test_urllib: %s\n" % self.__class__.__name__
  55. FILE = file(test_support.TESTFN, 'wb')
  56. try:
  57. FILE.write(self.text)
  58. finally:
  59. FILE.close()
  60. self.pathname = test_support.TESTFN
  61. self.returned_obj = urllib.urlopen("file:%s" % self.pathname)
  62. def tearDown(self):
  63. """Shut down the open object"""
  64. self.returned_obj.close()
  65. os.remove(test_support.TESTFN)
  66. def test_interface(self):
  67. # Make sure object returned by urlopen() has the specified methods
  68. for attr in ("read", "readline", "readlines", "fileno",
  69. "close", "info", "geturl", "getcode", "__iter__"):
  70. self.assertTrue(hasattr(self.returned_obj, attr),
  71. "object returned by urlopen() lacks %s attribute" %
  72. attr)
  73. def test_read(self):
  74. self.assertEqual(self.text, self.returned_obj.read())
  75. def test_readline(self):
  76. self.assertEqual(self.text, self.returned_obj.readline())
  77. self.assertEqual('', self.returned_obj.readline(),
  78. "calling readline() after exhausting the file did not"
  79. " return an empty string")
  80. def test_readlines(self):
  81. lines_list = self.returned_obj.readlines()
  82. self.assertEqual(len(lines_list), 1,
  83. "readlines() returned the wrong number of lines")
  84. self.assertEqual(lines_list[0], self.text,
  85. "readlines() returned improper text")
  86. def test_fileno(self):
  87. file_num = self.returned_obj.fileno()
  88. self.assertIsInstance(file_num, int, "fileno() did not return an int")
  89. self.assertEqual(os.read(file_num, len(self.text)), self.text,
  90. "Reading on the file descriptor returned by fileno() "
  91. "did not return the expected text")
  92. def test_close(self):
  93. # Test close() by calling it hear and then having it be called again
  94. # by the tearDown() method for the test
  95. self.returned_obj.close()
  96. def test_info(self):
  97. self.assertIsInstance(self.returned_obj.info(), mimetools.Message)
  98. def test_geturl(self):
  99. self.assertEqual(self.returned_obj.geturl(), self.pathname)
  100. def test_getcode(self):
  101. self.assertEqual(self.returned_obj.getcode(), None)
  102. def test_iter(self):
  103. # Test iterator
  104. # Don't need to count number of iterations since test would fail the
  105. # instant it returned anything beyond the first line from the
  106. # comparison
  107. for line in self.returned_obj.__iter__():
  108. self.assertEqual(line, self.text)
  109. def test_relativelocalfile(self):
  110. self.assertRaises(ValueError,urllib.urlopen,'./' + self.pathname)
  111. class ProxyTests(unittest.TestCase):
  112. def setUp(self):
  113. # Records changes to env vars
  114. self.env = test_support.EnvironmentVarGuard()
  115. # Delete all proxy related env vars
  116. for k in os.environ.keys():
  117. if 'proxy' in k.lower():
  118. self.env.unset(k)
  119. def tearDown(self):
  120. # Restore all proxy related env vars
  121. self.env.__exit__()
  122. del self.env
  123. def test_getproxies_environment_keep_no_proxies(self):
  124. self.env.set('NO_PROXY', 'localhost')
  125. proxies = urllib.getproxies_environment()
  126. # getproxies_environment use lowered case truncated (no '_proxy') keys
  127. self.assertEqual('localhost', proxies['no'])
  128. # List of no_proxies with space.
  129. self.env.set('NO_PROXY', 'localhost, anotherdomain.com, newdomain.com:1234')
  130. self.assertTrue(urllib.proxy_bypass_environment('anotherdomain.com'))
  131. self.assertTrue(urllib.proxy_bypass_environment('anotherdomain.com:8888'))
  132. self.assertTrue(urllib.proxy_bypass_environment('newdomain.com:1234'))
  133. def test_proxy_cgi_ignore(self):
  134. try:
  135. self.env.set('HTTP_PROXY', 'http://somewhere:3128')
  136. proxies = urllib.getproxies_environment()
  137. self.assertEqual('http://somewhere:3128', proxies['http'])
  138. self.env.set('REQUEST_METHOD', 'GET')
  139. proxies = urllib.getproxies_environment()
  140. self.assertNotIn('http', proxies)
  141. finally:
  142. self.env.unset('REQUEST_METHOD')
  143. self.env.unset('HTTP_PROXY')
  144. def test_proxy_bypass_environment_host_match(self):
  145. bypass = urllib.proxy_bypass_environment
  146. self.env.set('NO_PROXY',
  147. 'localhost, anotherdomain.com, newdomain.com:1234')
  148. self.assertTrue(bypass('localhost'))
  149. self.assertTrue(bypass('LocalHost')) # MixedCase
  150. self.assertTrue(bypass('LOCALHOST')) # UPPERCASE
  151. self.assertTrue(bypass('newdomain.com:1234'))
  152. self.assertTrue(bypass('anotherdomain.com:8888'))
  153. self.assertTrue(bypass('www.newdomain.com:1234'))
  154. self.assertFalse(bypass('prelocalhost'))
  155. self.assertFalse(bypass('newdomain.com')) # no port
  156. self.assertFalse(bypass('newdomain.com:1235')) # wrong port
  157. class ProxyTests_withOrderedEnv(unittest.TestCase):
  158. def setUp(self):
  159. # We need to test conditions, where variable order _is_ significant
  160. self._saved_env = os.environ
  161. # Monkey patch os.environ, start with empty fake environment
  162. os.environ = collections.OrderedDict()
  163. def tearDown(self):
  164. os.environ = self._saved_env
  165. def test_getproxies_environment_prefer_lowercase(self):
  166. # Test lowercase preference with removal
  167. os.environ['no_proxy'] = ''
  168. os.environ['No_Proxy'] = 'localhost'
  169. self.assertFalse(urllib.proxy_bypass_environment('localhost'))
  170. self.assertFalse(urllib.proxy_bypass_environment('arbitrary'))
  171. os.environ['http_proxy'] = ''
  172. os.environ['HTTP_PROXY'] = 'http://somewhere:3128'
  173. proxies = urllib.getproxies_environment()
  174. self.assertEqual({}, proxies)
  175. # Test lowercase preference of proxy bypass and correct matching including ports
  176. os.environ['no_proxy'] = 'localhost, noproxy.com, my.proxy:1234'
  177. os.environ['No_Proxy'] = 'xyz.com'
  178. self.assertTrue(urllib.proxy_bypass_environment('localhost'))
  179. self.assertTrue(urllib.proxy_bypass_environment('noproxy.com:5678'))
  180. self.assertTrue(urllib.proxy_bypass_environment('my.proxy:1234'))
  181. self.assertFalse(urllib.proxy_bypass_environment('my.proxy'))
  182. self.assertFalse(urllib.proxy_bypass_environment('arbitrary'))
  183. # Test lowercase preference with replacement
  184. os.environ['http_proxy'] = 'http://somewhere:3128'
  185. os.environ['Http_Proxy'] = 'http://somewhereelse:3128'
  186. proxies = urllib.getproxies_environment()
  187. self.assertEqual('http://somewhere:3128', proxies['http'])
  188. class urlopen_HttpTests(unittest.TestCase, FakeHTTPMixin):
  189. """Test urlopen() opening a fake http connection."""
  190. def test_read(self):
  191. self.fakehttp('Hello!')
  192. try:
  193. fp = urllib.urlopen("http://python.org/")
  194. self.assertEqual(fp.readline(), 'Hello!')
  195. self.assertEqual(fp.readline(), '')
  196. self.assertEqual(fp.geturl(), 'http://python.org/')
  197. self.assertEqual(fp.getcode(), 200)
  198. finally:
  199. self.unfakehttp()
  200. def test_url_fragment(self):
  201. # Issue #11703: geturl() omits fragments in the original URL.
  202. url = 'http://docs.python.org/library/urllib.html#OK'
  203. self.fakehttp('Hello!')
  204. try:
  205. fp = urllib.urlopen(url)
  206. self.assertEqual(fp.geturl(), url)
  207. finally:
  208. self.unfakehttp()
  209. def test_read_bogus(self):
  210. # urlopen() should raise IOError for many error codes.
  211. self.fakehttp('''HTTP/1.1 401 Authentication Required
  212. Date: Wed, 02 Jan 2008 03:03:54 GMT
  213. Server: Apache/1.3.33 (Debian GNU/Linux) mod_ssl/2.8.22 OpenSSL/0.9.7e
  214. Connection: close
  215. Content-Type: text/html; charset=iso-8859-1
  216. ''')
  217. try:
  218. self.assertRaises(IOError, urllib.urlopen, "http://python.org/")
  219. finally:
  220. self.unfakehttp()
  221. def test_invalid_redirect(self):
  222. # urlopen() should raise IOError for many error codes.
  223. self.fakehttp("""HTTP/1.1 302 Found
  224. Date: Wed, 02 Jan 2008 03:03:54 GMT
  225. Server: Apache/1.3.33 (Debian GNU/Linux) mod_ssl/2.8.22 OpenSSL/0.9.7e
  226. Location: file:README
  227. Connection: close
  228. Content-Type: text/html; charset=iso-8859-1
  229. """)
  230. try:
  231. msg = "Redirection to url 'file:"
  232. with self.assertRaisesRegexp(IOError, msg):
  233. urllib.urlopen("http://python.org/")
  234. finally:
  235. self.unfakehttp()
  236. def test_redirect_limit_independent(self):
  237. # Ticket #12923: make sure independent requests each use their
  238. # own retry limit.
  239. for i in range(urllib.FancyURLopener().maxtries):
  240. self.fakehttp(b'''HTTP/1.1 302 Found
  241. Location: file://guidocomputer.athome.com:/python/license
  242. Connection: close
  243. ''')
  244. try:
  245. self.assertRaises(IOError, urllib.urlopen,
  246. "http://something")
  247. finally:
  248. self.unfakehttp()
  249. def test_empty_socket(self):
  250. # urlopen() raises IOError if the underlying socket does not send any
  251. # data. (#1680230)
  252. self.fakehttp('')
  253. try:
  254. self.assertRaises(IOError, urllib.urlopen, 'http://something')
  255. finally:
  256. self.unfakehttp()
  257. def test_missing_localfile(self):
  258. self.assertRaises(IOError, urllib.urlopen,
  259. 'file://localhost/a/missing/file.py')
  260. fd, tmp_file = tempfile.mkstemp()
  261. tmp_fileurl = 'file://localhost/' + tmp_file.replace(os.path.sep, '/')
  262. self.assertTrue(os.path.exists(tmp_file))
  263. try:
  264. fp = urllib.urlopen(tmp_fileurl)
  265. fp.close()
  266. finally:
  267. os.close(fd)
  268. os.unlink(tmp_file)
  269. self.assertFalse(os.path.exists(tmp_file))
  270. self.assertRaises(IOError, urllib.urlopen, tmp_fileurl)
  271. def test_ftp_nonexisting(self):
  272. self.assertRaises(IOError, urllib.urlopen,
  273. 'ftp://localhost/not/existing/file.py')
  274. def test_userpass_inurl(self):
  275. self.fakehttp('Hello!')
  276. try:
  277. fakehttp_wrapper = httplib.HTTP._connection_class
  278. fp = urllib.urlopen("http://user:pass@python.org/")
  279. authorization = ("Authorization: Basic %s\r\n" %
  280. b64encode('user:pass'))
  281. # The authorization header must be in place
  282. self.assertIn(authorization, fakehttp_wrapper.buf)
  283. self.assertEqual(fp.readline(), "Hello!")
  284. self.assertEqual(fp.readline(), "")
  285. self.assertEqual(fp.geturl(), 'http://user:pass@python.org/')
  286. self.assertEqual(fp.getcode(), 200)
  287. finally:
  288. self.unfakehttp()
  289. def test_userpass_with_spaces_inurl(self):
  290. self.fakehttp('Hello!')
  291. try:
  292. url = "http://a b:c d@python.org/"
  293. fakehttp_wrapper = httplib.HTTP._connection_class
  294. authorization = ("Authorization: Basic %s\r\n" %
  295. b64encode('a b:c d'))
  296. fp = urllib.urlopen(url)
  297. # The authorization header must be in place
  298. self.assertIn(authorization, fakehttp_wrapper.buf)
  299. self.assertEqual(fp.readline(), "Hello!")
  300. self.assertEqual(fp.readline(), "")
  301. # the spaces are quoted in URL so no match
  302. self.assertNotEqual(fp.geturl(), url)
  303. self.assertEqual(fp.getcode(), 200)
  304. finally:
  305. self.unfakehttp()
  306. class urlretrieve_FileTests(unittest.TestCase):
  307. """Test urllib.urlretrieve() on local files"""
  308. def setUp(self):
  309. # Create a list of temporary files. Each item in the list is a file
  310. # name (absolute path or relative to the current working directory).
  311. # All files in this list will be deleted in the tearDown method. Note,
  312. # this only helps to makes sure temporary files get deleted, but it
  313. # does nothing about trying to close files that may still be open. It
  314. # is the responsibility of the developer to properly close files even
  315. # when exceptional conditions occur.
  316. self.tempFiles = []
  317. # Create a temporary file.
  318. self.registerFileForCleanUp(test_support.TESTFN)
  319. self.text = 'testing urllib.urlretrieve'
  320. try:
  321. FILE = file(test_support.TESTFN, 'wb')
  322. FILE.write(self.text)
  323. FILE.close()
  324. finally:
  325. try: FILE.close()
  326. except: pass
  327. def tearDown(self):
  328. # Delete the temporary files.
  329. for each in self.tempFiles:
  330. try: os.remove(each)
  331. except: pass
  332. def constructLocalFileUrl(self, filePath):
  333. return "file://%s" % urllib.pathname2url(os.path.abspath(filePath))
  334. def createNewTempFile(self, data=""):
  335. """Creates a new temporary file containing the specified data,
  336. registers the file for deletion during the test fixture tear down, and
  337. returns the absolute path of the file."""
  338. newFd, newFilePath = tempfile.mkstemp()
  339. try:
  340. self.registerFileForCleanUp(newFilePath)
  341. newFile = os.fdopen(newFd, "wb")
  342. newFile.write(data)
  343. newFile.close()
  344. finally:
  345. try: newFile.close()
  346. except: pass
  347. return newFilePath
  348. def registerFileForCleanUp(self, fileName):
  349. self.tempFiles.append(fileName)
  350. def test_basic(self):
  351. # Make sure that a local file just gets its own location returned and
  352. # a headers value is returned.
  353. result = urllib.urlretrieve("file:%s" % test_support.TESTFN)
  354. self.assertEqual(result[0], test_support.TESTFN)
  355. self.assertIsInstance(result[1], mimetools.Message,
  356. "did not get a mimetools.Message instance as "
  357. "second returned value")
  358. def test_copy(self):
  359. # Test that setting the filename argument works.
  360. second_temp = "%s.2" % test_support.TESTFN
  361. self.registerFileForCleanUp(second_temp)
  362. result = urllib.urlretrieve(self.constructLocalFileUrl(
  363. test_support.TESTFN), second_temp)
  364. self.assertEqual(second_temp, result[0])
  365. self.assertTrue(os.path.exists(second_temp), "copy of the file was not "
  366. "made")
  367. FILE = file(second_temp, 'rb')
  368. try:
  369. text = FILE.read()
  370. FILE.close()
  371. finally:
  372. try: FILE.close()
  373. except: pass
  374. self.assertEqual(self.text, text)
  375. def test_reporthook(self):
  376. # Make sure that the reporthook works.
  377. def hooktester(count, block_size, total_size, count_holder=[0]):
  378. self.assertIsInstance(count, int)
  379. self.assertIsInstance(block_size, int)
  380. self.assertIsInstance(total_size, int)
  381. self.assertEqual(count, count_holder[0])
  382. count_holder[0] = count_holder[0] + 1
  383. second_temp = "%s.2" % test_support.TESTFN
  384. self.registerFileForCleanUp(second_temp)
  385. urllib.urlretrieve(self.constructLocalFileUrl(test_support.TESTFN),
  386. second_temp, hooktester)
  387. def test_reporthook_0_bytes(self):
  388. # Test on zero length file. Should call reporthook only 1 time.
  389. report = []
  390. def hooktester(count, block_size, total_size, _report=report):
  391. _report.append((count, block_size, total_size))
  392. srcFileName = self.createNewTempFile()
  393. urllib.urlretrieve(self.constructLocalFileUrl(srcFileName),
  394. test_support.TESTFN, hooktester)
  395. self.assertEqual(len(report), 1)
  396. self.assertEqual(report[0][2], 0)
  397. def test_reporthook_5_bytes(self):
  398. # Test on 5 byte file. Should call reporthook only 2 times (once when
  399. # the "network connection" is established and once when the block is
  400. # read). Since the block size is 8192 bytes, only one block read is
  401. # required to read the entire file.
  402. report = []
  403. def hooktester(count, block_size, total_size, _report=report):
  404. _report.append((count, block_size, total_size))
  405. srcFileName = self.createNewTempFile("x" * 5)
  406. urllib.urlretrieve(self.constructLocalFileUrl(srcFileName),
  407. test_support.TESTFN, hooktester)
  408. self.assertEqual(len(report), 2)
  409. self.assertEqual(report[0][1], 8192)
  410. self.assertEqual(report[0][2], 5)
  411. def test_reporthook_8193_bytes(self):
  412. # Test on 8193 byte file. Should call reporthook only 3 times (once
  413. # when the "network connection" is established, once for the next 8192
  414. # bytes, and once for the last byte).
  415. report = []
  416. def hooktester(count, block_size, total_size, _report=report):
  417. _report.append((count, block_size, total_size))
  418. srcFileName = self.createNewTempFile("x" * 8193)
  419. urllib.urlretrieve(self.constructLocalFileUrl(srcFileName),
  420. test_support.TESTFN, hooktester)
  421. self.assertEqual(len(report), 3)
  422. self.assertEqual(report[0][1], 8192)
  423. self.assertEqual(report[0][2], 8193)
  424. class urlretrieve_HttpTests(unittest.TestCase, FakeHTTPMixin):
  425. """Test urllib.urlretrieve() using fake http connections"""
  426. def test_short_content_raises_ContentTooShortError(self):
  427. self.fakehttp('''HTTP/1.1 200 OK
  428. Date: Wed, 02 Jan 2008 03:03:54 GMT
  429. Server: Apache/1.3.33 (Debian GNU/Linux) mod_ssl/2.8.22 OpenSSL/0.9.7e
  430. Connection: close
  431. Content-Length: 100
  432. Content-Type: text/html; charset=iso-8859-1
  433. FF
  434. ''')
  435. def _reporthook(par1, par2, par3):
  436. pass
  437. try:
  438. self.assertRaises(urllib.ContentTooShortError, urllib.urlretrieve,
  439. 'http://example.com', reporthook=_reporthook)
  440. finally:
  441. self.unfakehttp()
  442. def test_short_content_raises_ContentTooShortError_without_reporthook(self):
  443. self.fakehttp('''HTTP/1.1 200 OK
  444. Date: Wed, 02 Jan 2008 03:03:54 GMT
  445. Server: Apache/1.3.33 (Debian GNU/Linux) mod_ssl/2.8.22 OpenSSL/0.9.7e
  446. Connection: close
  447. Content-Length: 100
  448. Content-Type: text/html; charset=iso-8859-1
  449. FF
  450. ''')
  451. try:
  452. self.assertRaises(urllib.ContentTooShortError, urllib.urlretrieve, 'http://example.com/')
  453. finally:
  454. self.unfakehttp()
  455. class QuotingTests(unittest.TestCase):
  456. """Tests for urllib.quote() and urllib.quote_plus()
  457. According to RFC 2396 ("Uniform Resource Identifiers), to escape a
  458. character you write it as '%' + <2 character US-ASCII hex value>. The Python
  459. code of ``'%' + hex(ord(<character>))[2:]`` escapes a character properly.
  460. Case does not matter on the hex letters.
  461. The various character sets specified are:
  462. Reserved characters : ";/?:@&=+$,"
  463. Have special meaning in URIs and must be escaped if not being used for
  464. their special meaning
  465. Data characters : letters, digits, and "-_.!~*'()"
  466. Unreserved and do not need to be escaped; can be, though, if desired
  467. Control characters : 0x00 - 0x1F, 0x7F
  468. Have no use in URIs so must be escaped
  469. space : 0x20
  470. Must be escaped
  471. Delimiters : '<>#%"'
  472. Must be escaped
  473. Unwise : "{}|\^[]`"
  474. Must be escaped
  475. """
  476. def test_never_quote(self):
  477. # Make sure quote() does not quote letters, digits, and "_,.-"
  478. do_not_quote = '' .join(["ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  479. "abcdefghijklmnopqrstuvwxyz",
  480. "0123456789",
  481. "_.-"])
  482. result = urllib.quote(do_not_quote)
  483. self.assertEqual(do_not_quote, result,
  484. "using quote(): %s != %s" % (do_not_quote, result))
  485. result = urllib.quote_plus(do_not_quote)
  486. self.assertEqual(do_not_quote, result,
  487. "using quote_plus(): %s != %s" % (do_not_quote, result))
  488. def test_default_safe(self):
  489. # Test '/' is default value for 'safe' parameter
  490. self.assertEqual(urllib.quote.func_defaults[0], '/')
  491. def test_safe(self):
  492. # Test setting 'safe' parameter does what it should do
  493. quote_by_default = "<>"
  494. result = urllib.quote(quote_by_default, safe=quote_by_default)
  495. self.assertEqual(quote_by_default, result,
  496. "using quote(): %s != %s" % (quote_by_default, result))
  497. result = urllib.quote_plus(quote_by_default, safe=quote_by_default)
  498. self.assertEqual(quote_by_default, result,
  499. "using quote_plus(): %s != %s" %
  500. (quote_by_default, result))
  501. def test_default_quoting(self):
  502. # Make sure all characters that should be quoted are by default sans
  503. # space (separate test for that).
  504. should_quote = [chr(num) for num in range(32)] # For 0x00 - 0x1F
  505. should_quote.append('<>#%"{}|\^[]`')
  506. should_quote.append(chr(127)) # For 0x7F
  507. should_quote = ''.join(should_quote)
  508. for char in should_quote:
  509. result = urllib.quote(char)
  510. self.assertEqual(hexescape(char), result,
  511. "using quote(): %s should be escaped to %s, not %s" %
  512. (char, hexescape(char), result))
  513. result = urllib.quote_plus(char)
  514. self.assertEqual(hexescape(char), result,
  515. "using quote_plus(): "
  516. "%s should be escapes to %s, not %s" %
  517. (char, hexescape(char), result))
  518. del should_quote
  519. partial_quote = "ab[]cd"
  520. expected = "ab%5B%5Dcd"
  521. result = urllib.quote(partial_quote)
  522. self.assertEqual(expected, result,
  523. "using quote(): %s != %s" % (expected, result))
  524. result = urllib.quote_plus(partial_quote)
  525. self.assertEqual(expected, result,
  526. "using quote_plus(): %s != %s" % (expected, result))
  527. self.assertRaises(TypeError, urllib.quote, None)
  528. def test_quoting_space(self):
  529. # Make sure quote() and quote_plus() handle spaces as specified in
  530. # their unique way
  531. result = urllib.quote(' ')
  532. self.assertEqual(result, hexescape(' '),
  533. "using quote(): %s != %s" % (result, hexescape(' ')))
  534. result = urllib.quote_plus(' ')
  535. self.assertEqual(result, '+',
  536. "using quote_plus(): %s != +" % result)
  537. given = "a b cd e f"
  538. expect = given.replace(' ', hexescape(' '))
  539. result = urllib.quote(given)
  540. self.assertEqual(expect, result,
  541. "using quote(): %s != %s" % (expect, result))
  542. expect = given.replace(' ', '+')
  543. result = urllib.quote_plus(given)
  544. self.assertEqual(expect, result,
  545. "using quote_plus(): %s != %s" % (expect, result))
  546. def test_quoting_plus(self):
  547. self.assertEqual(urllib.quote_plus('alpha+beta gamma'),
  548. 'alpha%2Bbeta+gamma')
  549. self.assertEqual(urllib.quote_plus('alpha+beta gamma', '+'),
  550. 'alpha+beta+gamma')
  551. class UnquotingTests(unittest.TestCase):
  552. """Tests for unquote() and unquote_plus()
  553. See the doc string for quoting_Tests for details on quoting and such.
  554. """
  555. def test_unquoting(self):
  556. # Make sure unquoting of all ASCII values works
  557. escape_list = []
  558. for num in range(128):
  559. given = hexescape(chr(num))
  560. expect = chr(num)
  561. result = urllib.unquote(given)
  562. self.assertEqual(expect, result,
  563. "using unquote(): %s != %s" % (expect, result))
  564. result = urllib.unquote_plus(given)
  565. self.assertEqual(expect, result,
  566. "using unquote_plus(): %s != %s" %
  567. (expect, result))
  568. escape_list.append(given)
  569. escape_string = ''.join(escape_list)
  570. del escape_list
  571. result = urllib.unquote(escape_string)
  572. self.assertEqual(result.count('%'), 1,
  573. "using quote(): not all characters escaped; %s" %
  574. result)
  575. result = urllib.unquote(escape_string)
  576. self.assertEqual(result.count('%'), 1,
  577. "using unquote(): not all characters escaped: "
  578. "%s" % result)
  579. def test_unquoting_badpercent(self):
  580. # Test unquoting on bad percent-escapes
  581. given = '%xab'
  582. expect = given
  583. result = urllib.unquote(given)
  584. self.assertEqual(expect, result, "using unquote(): %r != %r"
  585. % (expect, result))
  586. given = '%x'
  587. expect = given
  588. result = urllib.unquote(given)
  589. self.assertEqual(expect, result, "using unquote(): %r != %r"
  590. % (expect, result))
  591. given = '%'
  592. expect = given
  593. result = urllib.unquote(given)
  594. self.assertEqual(expect, result, "using unquote(): %r != %r"
  595. % (expect, result))
  596. def test_unquoting_mixed_case(self):
  597. # Test unquoting on mixed-case hex digits in the percent-escapes
  598. given = '%Ab%eA'
  599. expect = '\xab\xea'
  600. result = urllib.unquote(given)
  601. self.assertEqual(expect, result, "using unquote(): %r != %r"
  602. % (expect, result))
  603. def test_unquoting_parts(self):
  604. # Make sure unquoting works when have non-quoted characters
  605. # interspersed
  606. given = 'ab%sd' % hexescape('c')
  607. expect = "abcd"
  608. result = urllib.unquote(given)
  609. self.assertEqual(expect, result,
  610. "using quote(): %s != %s" % (expect, result))
  611. result = urllib.unquote_plus(given)
  612. self.assertEqual(expect, result,
  613. "using unquote_plus(): %s != %s" % (expect, result))
  614. def test_unquoting_plus(self):
  615. # Test difference between unquote() and unquote_plus()
  616. given = "are+there+spaces..."
  617. expect = given
  618. result = urllib.unquote(given)
  619. self.assertEqual(expect, result,
  620. "using unquote(): %s != %s" % (expect, result))
  621. expect = given.replace('+', ' ')
  622. result = urllib.unquote_plus(given)
  623. self.assertEqual(expect, result,
  624. "using unquote_plus(): %s != %s" % (expect, result))
  625. def test_unquote_with_unicode(self):
  626. r = urllib.unquote(u'br%C3%BCckner_sapporo_20050930.doc')
  627. self.assertEqual(r, u'br\xc3\xbcckner_sapporo_20050930.doc')
  628. class urlencode_Tests(unittest.TestCase):
  629. """Tests for urlencode()"""
  630. def help_inputtype(self, given, test_type):
  631. """Helper method for testing different input types.
  632. 'given' must lead to only the pairs:
  633. * 1st, 1
  634. * 2nd, 2
  635. * 3rd, 3
  636. Test cannot assume anything about order. Docs make no guarantee and
  637. have possible dictionary input.
  638. """
  639. expect_somewhere = ["1st=1", "2nd=2", "3rd=3"]
  640. result = urllib.urlencode(given)
  641. for expected in expect_somewhere:
  642. self.assertIn(expected, result,
  643. "testing %s: %s not found in %s" %
  644. (test_type, expected, result))
  645. self.assertEqual(result.count('&'), 2,
  646. "testing %s: expected 2 '&'s; got %s" %
  647. (test_type, result.count('&')))
  648. amp_location = result.index('&')
  649. on_amp_left = result[amp_location - 1]
  650. on_amp_right = result[amp_location + 1]
  651. self.assertTrue(on_amp_left.isdigit() and on_amp_right.isdigit(),
  652. "testing %s: '&' not located in proper place in %s" %
  653. (test_type, result))
  654. self.assertEqual(len(result), (5 * 3) + 2, #5 chars per thing and amps
  655. "testing %s: "
  656. "unexpected number of characters: %s != %s" %
  657. (test_type, len(result), (5 * 3) + 2))
  658. def test_using_mapping(self):
  659. # Test passing in a mapping object as an argument.
  660. self.help_inputtype({"1st":'1', "2nd":'2', "3rd":'3'},
  661. "using dict as input type")
  662. def test_using_sequence(self):
  663. # Test passing in a sequence of two-item sequences as an argument.
  664. self.help_inputtype([('1st', '1'), ('2nd', '2'), ('3rd', '3')],
  665. "using sequence of two-item tuples as input")
  666. def test_quoting(self):
  667. # Make sure keys and values are quoted using quote_plus()
  668. given = {"&":"="}
  669. expect = "%s=%s" % (hexescape('&'), hexescape('='))
  670. result = urllib.urlencode(given)
  671. self.assertEqual(expect, result)
  672. given = {"key name":"A bunch of pluses"}
  673. expect = "key+name=A+bunch+of+pluses"
  674. result = urllib.urlencode(given)
  675. self.assertEqual(expect, result)
  676. def test_doseq(self):
  677. # Test that passing True for 'doseq' parameter works correctly
  678. given = {'sequence':['1', '2', '3']}
  679. expect = "sequence=%s" % urllib.quote_plus(str(['1', '2', '3']))
  680. result = urllib.urlencode(given)
  681. self.assertEqual(expect, result)
  682. result = urllib.urlencode(given, True)
  683. for value in given["sequence"]:
  684. expect = "sequence=%s" % value
  685. self.assertIn(expect, result)
  686. self.assertEqual(result.count('&'), 2,
  687. "Expected 2 '&'s, got %s" % result.count('&'))
  688. class Pathname_Tests(unittest.TestCase):
  689. """Test pathname2url() and url2pathname()"""
  690. def test_basic(self):
  691. # Make sure simple tests pass
  692. expected_path = os.path.join("parts", "of", "a", "path")
  693. expected_url = "parts/of/a/path"
  694. result = urllib.pathname2url(expected_path)
  695. self.assertEqual(expected_url, result,
  696. "pathname2url() failed; %s != %s" %
  697. (result, expected_url))
  698. result = urllib.url2pathname(expected_url)
  699. self.assertEqual(expected_path, result,
  700. "url2pathame() failed; %s != %s" %
  701. (result, expected_path))
  702. def test_quoting(self):
  703. # Test automatic quoting and unquoting works for pathnam2url() and
  704. # url2pathname() respectively
  705. given = os.path.join("needs", "quot=ing", "here")
  706. expect = "needs/%s/here" % urllib.quote("quot=ing")
  707. result = urllib.pathname2url(given)
  708. self.assertEqual(expect, result,
  709. "pathname2url() failed; %s != %s" %
  710. (expect, result))
  711. expect = given
  712. result = urllib.url2pathname(result)
  713. self.assertEqual(expect, result,
  714. "url2pathname() failed; %s != %s" %
  715. (expect, result))
  716. given = os.path.join("make sure", "using_quote")
  717. expect = "%s/using_quote" % urllib.quote("make sure")
  718. result = urllib.pathname2url(given)
  719. self.assertEqual(expect, result,
  720. "pathname2url() failed; %s != %s" %
  721. (expect, result))
  722. given = "make+sure/using_unquote"
  723. expect = os.path.join("make+sure", "using_unquote")
  724. result = urllib.url2pathname(given)
  725. self.assertEqual(expect, result,
  726. "url2pathname() failed; %s != %s" %
  727. (expect, result))
  728. @unittest.skipUnless(sys.platform == 'win32',
  729. 'test specific to the nturl2path library')
  730. def test_ntpath(self):
  731. given = ('/C:/', '///C:/', '/C|//')
  732. expect = 'C:\\'
  733. for url in given:
  734. result = urllib.url2pathname(url)
  735. self.assertEqual(expect, result,
  736. 'nturl2path.url2pathname() failed; %s != %s' %
  737. (expect, result))
  738. given = '///C|/path'
  739. expect = 'C:\\path'
  740. result = urllib.url2pathname(given)
  741. self.assertEqual(expect, result,
  742. 'nturl2path.url2pathname() failed; %s != %s' %
  743. (expect, result))
  744. class Utility_Tests(unittest.TestCase):
  745. """Testcase to test the various utility functions in the urllib."""
  746. # In Python 3 this test class is moved to test_urlparse.
  747. def test_splittype(self):
  748. splittype = urllib.splittype
  749. self.assertEqual(splittype('type:opaquestring'), ('type', 'opaquestring'))
  750. self.assertEqual(splittype('opaquestring'), (None, 'opaquestring'))
  751. self.assertEqual(splittype(':opaquestring'), (None, ':opaquestring'))
  752. self.assertEqual(splittype('type:'), ('type', ''))
  753. self.assertEqual(splittype('type:opaque:string'), ('type', 'opaque:string'))
  754. def test_splithost(self):
  755. splithost = urllib.splithost
  756. self.assertEqual(splithost('//www.example.org:80/foo/bar/baz.html'),
  757. ('www.example.org:80', '/foo/bar/baz.html'))
  758. self.assertEqual(splithost('//www.example.org:80'),
  759. ('www.example.org:80', ''))
  760. self.assertEqual(splithost('/foo/bar/baz.html'),
  761. (None, '/foo/bar/baz.html'))
  762. def test_splituser(self):
  763. splituser = urllib.splituser
  764. self.assertEqual(splituser('User:Pass@www.python.org:080'),
  765. ('User:Pass', 'www.python.org:080'))
  766. self.assertEqual(splituser('@www.python.org:080'),
  767. ('', 'www.python.org:080'))
  768. self.assertEqual(splituser('www.python.org:080'),
  769. (None, 'www.python.org:080'))
  770. self.assertEqual(splituser('User:Pass@'),
  771. ('User:Pass', ''))
  772. self.assertEqual(splituser('User@example.com:Pass@www.python.org:080'),
  773. ('User@example.com:Pass', 'www.python.org:080'))
  774. def test_splitpasswd(self):
  775. # Some of the password examples are not sensible, but it is added to
  776. # confirming to RFC2617 and addressing issue4675.
  777. splitpasswd = urllib.splitpasswd
  778. self.assertEqual(splitpasswd('user:ab'), ('user', 'ab'))
  779. self.assertEqual(splitpasswd('user:a\nb'), ('user', 'a\nb'))
  780. self.assertEqual(splitpasswd('user:a\tb'), ('user', 'a\tb'))
  781. self.assertEqual(splitpasswd('user:a\rb'), ('user', 'a\rb'))
  782. self.assertEqual(splitpasswd('user:a\fb'), ('user', 'a\fb'))
  783. self.assertEqual(splitpasswd('user:a\vb'), ('user', 'a\vb'))
  784. self.assertEqual(splitpasswd('user:a:b'), ('user', 'a:b'))
  785. self.assertEqual(splitpasswd('user:a b'), ('user', 'a b'))
  786. self.assertEqual(splitpasswd('user 2:ab'), ('user 2', 'ab'))
  787. self.assertEqual(splitpasswd('user+1:a+b'), ('user+1', 'a+b'))
  788. self.assertEqual(splitpasswd('user:'), ('user', ''))
  789. self.assertEqual(splitpasswd('user'), ('user', None))
  790. self.assertEqual(splitpasswd(':ab'), ('', 'ab'))
  791. def test_splitport(self):
  792. splitport = urllib.splitport
  793. self.assertEqual(splitport('parrot:88'), ('parrot', '88'))
  794. self.assertEqual(splitport('parrot'), ('parrot', None))
  795. self.assertEqual(splitport('parrot:'), ('parrot', None))
  796. self.assertEqual(splitport('127.0.0.1'), ('127.0.0.1', None))
  797. self.assertEqual(splitport('parrot:cheese'), ('parrot:cheese', None))
  798. self.assertEqual(splitport('[::1]:88'), ('[::1]', '88'))
  799. self.assertEqual(splitport('[::1]'), ('[::1]', None))
  800. self.assertEqual(splitport(':88'), ('', '88'))
  801. def test_splitnport(self):
  802. splitnport = urllib.splitnport
  803. self.assertEqual(splitnport('parrot:88'), ('parrot', 88))
  804. self.assertEqual(splitnport('parrot'), ('parrot', -1))
  805. self.assertEqual(splitnport('parrot', 55), ('parrot', 55))
  806. self.assertEqual(splitnport('parrot:'), ('parrot', -1))
  807. self.assertEqual(splitnport('parrot:', 55), ('parrot', 55))
  808. self.assertEqual(splitnport('127.0.0.1'), ('127.0.0.1', -1))
  809. self.assertEqual(splitnport('127.0.0.1', 55), ('127.0.0.1', 55))
  810. self.assertEqual(splitnport('parrot:cheese'), ('parrot', None))
  811. self.assertEqual(splitnport('parrot:cheese', 55), ('parrot', None))
  812. def test_splitquery(self):
  813. # Normal cases are exercised by other tests; ensure that we also
  814. # catch cases with no port specified (testcase ensuring coverage)
  815. splitquery = urllib.splitquery
  816. self.assertEqual(splitquery('http://python.org/fake?foo=bar'),
  817. ('http://python.org/fake', 'foo=bar'))
  818. self.assertEqual(splitquery('http://python.org/fake?foo=bar?'),
  819. ('http://python.org/fake?foo=bar', ''))
  820. self.assertEqual(splitquery('http://python.org/fake'),
  821. ('http://python.org/fake', None))
  822. self.assertEqual(splitquery('?foo=bar'), ('', 'foo=bar'))
  823. def test_splittag(self):
  824. splittag = urllib.splittag
  825. self.assertEqual(splittag('http://example.com?foo=bar#baz'),
  826. ('http://example.com?foo=bar', 'baz'))
  827. self.assertEqual(splittag('http://example.com?foo=bar#'),
  828. ('http://example.com?foo=bar', ''))
  829. self.assertEqual(splittag('#baz'), ('', 'baz'))
  830. self.assertEqual(splittag('http://example.com?foo=bar'),
  831. ('http://example.com?foo=bar', None))
  832. self.assertEqual(splittag('http://example.com?foo=bar#baz#boo'),
  833. ('http://example.com?foo=bar#baz', 'boo'))
  834. def test_splitattr(self):
  835. splitattr = urllib.splitattr
  836. self.assertEqual(splitattr('/path;attr1=value1;attr2=value2'),
  837. ('/path', ['attr1=value1', 'attr2=value2']))
  838. self.assertEqual(splitattr('/path;'), ('/path', ['']))
  839. self.assertEqual(splitattr(';attr1=value1;attr2=value2'),
  840. ('', ['attr1=value1', 'attr2=value2']))
  841. self.assertEqual(splitattr('/path'), ('/path', []))
  842. def test_splitvalue(self):
  843. # Normal cases are exercised by other tests; test pathological cases
  844. # with no key/value pairs. (testcase ensuring coverage)
  845. splitvalue = urllib.splitvalue
  846. self.assertEqual(splitvalue('foo=bar'), ('foo', 'bar'))
  847. self.assertEqual(splitvalue('foo='), ('foo', ''))
  848. self.assertEqual(splitvalue('=bar'), ('', 'bar'))
  849. self.assertEqual(splitvalue('foobar'), ('foobar', None))
  850. self.assertEqual(splitvalue('foo=bar=baz'), ('foo', 'bar=baz'))
  851. def test_toBytes(self):
  852. result = urllib.toBytes(u'http://www.python.org')
  853. self.assertEqual(result, 'http://www.python.org')
  854. self.assertRaises(UnicodeError, urllib.toBytes,
  855. test_support.u(r'http://www.python.org/medi\u00e6val'))
  856. def test_unwrap(self):
  857. url = urllib.unwrap('<URL:type://host/path>')
  858. self.assertEqual(url, 'type://host/path')
  859. class URLopener_Tests(unittest.TestCase):
  860. """Testcase to test the open method of URLopener class."""
  861. def test_quoted_open(self):
  862. class DummyURLopener(urllib.URLopener):
  863. def open_spam(self, url):
  864. return url
  865. self.assertEqual(DummyURLopener().open(
  866. 'spam://example/ /'),'//example/%20/')
  867. # test the safe characters are not quoted by urlopen
  868. self.assertEqual(DummyURLopener().open(
  869. "spam://c:|windows%/:=&?~#+!$,;'@()*[]|/path/"),
  870. "//c:|windows%/:=&?~#+!$,;'@()*[]|/path/")
  871. # Just commented them out.
  872. # Can't really tell why keep failing in windows and sparc.
  873. # Everywhere else they work ok, but on those machines, sometimes
  874. # fail in one of the tests, sometimes in other. I have a linux, and
  875. # the tests go ok.
  876. # If anybody has one of the problematic environments, please help!
  877. # . Facundo
  878. #
  879. # def server(evt):
  880. # import socket, time
  881. # serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  882. # serv.settimeout(3)
  883. # serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  884. # serv.bind(("", 9093))
  885. # serv.listen(5)
  886. # try:
  887. # conn, addr = serv.accept()
  888. # conn.send("1 Hola mundo\n")
  889. # cantdata = 0
  890. # while cantdata < 13:
  891. # data = conn.recv(13-cantdata)
  892. # cantdata += len(data)
  893. # time.sleep(.3)
  894. # conn.send("2 No more lines\n")
  895. # conn.close()
  896. # except socket.timeout:
  897. # pass
  898. # finally:
  899. # serv.close()
  900. # evt.set()
  901. #
  902. # class FTPWrapperTests(unittest.TestCase):
  903. #
  904. # def setUp(self):
  905. # import ftplib, time, threading
  906. # ftplib.FTP.port = 9093
  907. # self.evt = threading.Event()
  908. # threading.Thread(target=server, args=(self.evt,)).start()
  909. # time.sleep(.1)
  910. #
  911. # def tearDown(self):
  912. # self.evt.wait()
  913. #
  914. # def testBasic(self):
  915. # # connects
  916. # ftp = urllib.ftpwrapper("myuser", "mypass", "localhost", 9093, [])
  917. # ftp.close()
  918. #
  919. # def testTimeoutNone(self):
  920. # # global default timeout is ignored
  921. # import socket
  922. # self.assertIsNone(socket.getdefaulttimeout())
  923. # socket.setdefaulttimeout(30)
  924. # try:
  925. # ftp = urllib.ftpwrapper("myuser", "mypass", "localhost", 9093, [])
  926. # finally:
  927. # socket.setdefaulttimeout(None)
  928. # self.assertEqual(ftp.ftp.sock.gettimeout(), 30)
  929. # ftp.close()
  930. #
  931. # def testTimeoutDefault(self):
  932. # # global default timeout is used
  933. # import socket
  934. # self.assertIsNone(socket.getdefaulttimeout())
  935. # socket.setdefaulttimeout(30)
  936. # try:
  937. # ftp = urllib.ftpwrapper("myuser", "mypass", "localhost", 9093, [])
  938. # finally:
  939. # socket.setdefaulttimeout(None)
  940. # self.assertEqual(ftp.ftp.sock.gettimeout(), 30)
  941. # ftp.close()
  942. #
  943. # def testTimeoutValue(self):
  944. # ftp = urllib.ftpwrapper("myuser", "mypass", "localhost", 9093, [],
  945. # timeout=30)
  946. # self.assertEqual(ftp.ftp.sock.gettimeout(), 30)
  947. # ftp.close()
  948. def test_main():
  949. import warnings
  950. with warnings.catch_warnings():
  951. warnings.filterwarnings('ignore', ".*urllib\.urlopen.*Python 3.0",
  952. DeprecationWarning)
  953. test_support.run_unittest(
  954. urlopen_FileTests,
  955. urlopen_HttpTests,
  956. urlretrieve_FileTests,
  957. urlretrieve_HttpTests,
  958. ProxyTests,
  959. QuotingTests,
  960. UnquotingTests,
  961. urlencode_Tests,
  962. Pathname_Tests,
  963. Utility_Tests,
  964. URLopener_Tests,
  965. ProxyTests,
  966. ProxyTests_withOrderedEnv,
  967. #FTPWrapperTests,
  968. )
  969. if __name__ == '__main__':
  970. test_main()