Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bd576b7

Browse files
committed
Fix issue #4972: adds ftplib.FTP context manager protocol
1 parent f95a1b3 commit bd576b7

5 files changed

Lines changed: 99 additions & 11 deletions

File tree

Doc/library/ftplib.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ The module defines the following items:
4646
connection attempt (if is not specified, the global default timeout setting
4747
will be used).
4848

49+
:class:`FTP` class supports the :keyword:`with` statement. Here is a sample
50+
on how using it:
51+
52+
>>> from ftplib import FTP
53+
>>> with FTP("ftp1.at.proftpd.org") as ftp:
54+
... ftp.login()
55+
... ftp.dir()
56+
...
57+
'230 Anonymous login ok, restrictions apply.'
58+
dr-xr-xr-x 9 ftp ftp 154 May 6 10:43 .
59+
dr-xr-xr-x 9 ftp ftp 154 May 6 10:43 ..
60+
dr-xr-xr-x 5 ftp ftp 4096 May 6 10:43 CentOS
61+
dr-xr-xr-x 3 ftp ftp 18 Jul 10 2008 Fedora
62+
>>>
63+
64+
.. versionchanged:: 3.2
65+
Support for the :keyword:`with` statement was added.
66+
67+
4968
.. class:: FTP_TLS(host='', user='', passwd='', acct='', [keyfile[, certfile[, timeout]]])
5069

5170
A :class:`FTP` subclass which adds TLS support to FTP as described in

Doc/whatsnew/3.2.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ Some smaller changes made to the core Python language are:
6666
New, Improved, and Deprecated Modules
6767
=====================================
6868

69+
* The :class:`ftplib.FTP` class now supports the context manager protocol
70+
(Contributed by Tarek Ziadé and Giampaolo Rodolà; :issue:`4972`.)
71+
6972
* The previously deprecated :func:`string.maketrans` function has been
7073
removed in favor of the static methods, :meth:`bytes.maketrans` and
7174
:meth:`bytearray.maketrans`. This change solves the confusion around which

Lib/ftplib.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ def __init__(self, host='', user='', passwd='', acct='',
120120
if user:
121121
self.login(user, passwd, acct)
122122

123+
def __enter__(self):
124+
return self
125+
126+
# Context management protocol: try to quit() if active
127+
def __exit__(self, *args):
128+
if self.sock is not None:
129+
try:
130+
self.quit()
131+
except (socket.error, EOFError):
132+
pass
133+
finally:
134+
if self.sock is not None:
135+
self.close()
136+
123137
def connect(self, host='', port=0, timeout=-999):
124138
'''Connect to host. Arguments are:
125139
- host: hostname to connect to (string, default previous host)

Lib/test/test_ftplib.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io
1111
import errno
1212
import os
13+
import time
1314
try:
1415
import ssl
1516
except ImportError:
@@ -137,6 +138,9 @@ def cmd_echo(self, arg):
137138
# sends back the received string (used by the test suite)
138139
self.push(arg)
139140

141+
def cmd_noop(self, arg):
142+
self.push('200 noop ok')
143+
140144
def cmd_user(self, arg):
141145
self.push('331 username ok')
142146

@@ -218,6 +222,7 @@ def __init__(self, address, af=socket.AF_INET):
218222
self.active = False
219223
self.active_lock = threading.Lock()
220224
self.host, self.port = self.socket.getsockname()[:2]
225+
self.handler_instance = None
221226

222227
def start(self):
223228
assert not self.active
@@ -241,8 +246,7 @@ def stop(self):
241246

242247
def handle_accept(self):
243248
conn, addr = self.accept()
244-
self.handler = self.handler(conn)
245-
self.close()
249+
self.handler_instance = self.handler(conn)
246250

247251
def handle_connect(self):
248252
self.close()
@@ -459,12 +463,12 @@ def test_acct(self):
459463

460464
def test_rename(self):
461465
self.client.rename('a', 'b')
462-
self.server.handler.next_response = '200'
466+
self.server.handler_instance.next_response = '200'
463467
self.assertRaises(ftplib.error_reply, self.client.rename, 'a', 'b')
464468

465469
def test_delete(self):
466470
self.client.delete('foo')
467-
self.server.handler.next_response = '199'
471+
self.server.handler_instance.next_response = '199'
468472
self.assertRaises(ftplib.error_reply, self.client.delete, 'foo')
469473

470474
def test_size(self):
@@ -512,7 +516,7 @@ def test_retrlines(self):
512516
def test_storbinary(self):
513517
f = io.BytesIO(RETR_DATA.encode('ascii'))
514518
self.client.storbinary('stor', f)
515-
self.assertEqual(self.server.handler.last_received_data, RETR_DATA)
519+
self.assertEqual(self.server.handler_instance.last_received_data, RETR_DATA)
516520
# test new callback arg
517521
flag = []
518522
f.seek(0)
@@ -524,12 +528,12 @@ def test_storbinary_rest(self):
524528
for r in (30, '30'):
525529
f.seek(0)
526530
self.client.storbinary('stor', f, rest=r)
527-
self.assertEqual(self.server.handler.rest, str(r))
531+
self.assertEqual(self.server.handler_instance.rest, str(r))
528532

529533
def test_storlines(self):
530534
f = io.BytesIO(RETR_DATA.replace('\r\n', '\n').encode('ascii'))
531535
self.client.storlines('stor', f)
532-
self.assertEqual(self.server.handler.last_received_data, RETR_DATA)
536+
self.assertEqual(self.server.handler_instance.last_received_data, RETR_DATA)
533537
# test new callback arg
534538
flag = []
535539
f.seek(0)
@@ -548,14 +552,59 @@ def test_dir(self):
548552
def test_makeport(self):
549553
self.client.makeport()
550554
# IPv4 is in use, just make sure send_eprt has not been used
551-
self.assertEqual(self.server.handler.last_received_cmd, 'port')
555+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'port')
552556

553557
def test_makepasv(self):
554558
host, port = self.client.makepasv()
555559
conn = socket.create_connection((host, port), 2)
556560
conn.close()
557561
# IPv4 is in use, just make sure send_epsv has not been used
558-
self.assertEqual(self.server.handler.last_received_cmd, 'pasv')
562+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'pasv')
563+
564+
def test_with_statement(self):
565+
self.client.quit()
566+
567+
def is_client_connected():
568+
if self.client.sock is None:
569+
return False
570+
try:
571+
self.client.sendcmd('noop')
572+
except (socket.error, EOFError):
573+
return False
574+
return True
575+
576+
# base test
577+
with ftplib.FTP(timeout=2) as self.client:
578+
self.client.connect(self.server.host, self.server.port)
579+
self.client.sendcmd('noop')
580+
self.assertTrue(is_client_connected())
581+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'quit')
582+
self.assertFalse(is_client_connected())
583+
584+
# QUIT sent inside the with block
585+
with ftplib.FTP(timeout=2) as self.client:
586+
self.client.connect(self.server.host, self.server.port)
587+
self.client.sendcmd('noop')
588+
self.client.quit()
589+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'quit')
590+
self.assertFalse(is_client_connected())
591+
592+
# force a wrong response code to be sent on QUIT: error_perm
593+
# is expected and the connection is supposed to be closed
594+
try:
595+
with ftplib.FTP(timeout=2) as self.client:
596+
self.client.connect(self.server.host, self.server.port)
597+
self.client.sendcmd('noop')
598+
self.server.handler_instance.next_response = '550 error on quit'
599+
except ftplib.error_perm as err:
600+
self.assertEqual(str(err), '550 error on quit')
601+
else:
602+
self.fail('Exception not raised')
603+
# needed to give the threaded server some time to set the attribute
604+
# which otherwise would still be == 'noop'
605+
time.sleep(0.1)
606+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'quit')
607+
self.assertFalse(is_client_connected())
559608

560609

561610
class TestIPv6Environment(TestCase):
@@ -575,13 +624,13 @@ def test_af(self):
575624

576625
def test_makeport(self):
577626
self.client.makeport()
578-
self.assertEqual(self.server.handler.last_received_cmd, 'eprt')
627+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'eprt')
579628

580629
def test_makepasv(self):
581630
host, port = self.client.makepasv()
582631
conn = socket.create_connection((host, port), 2)
583632
conn.close()
584-
self.assertEqual(self.server.handler.last_received_cmd, 'epsv')
633+
self.assertEqual(self.server.handler_instance.last_received_cmd, 'epsv')
585634

586635
def test_transfer(self):
587636
def retr():

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,9 @@ C-API
351351
Library
352352
-------
353353

354+
- Issue #4972: Add support for the context manager protocol to the ftplib.FTP
355+
class.
356+
354357
- Issue #8664: In py_compile, create __pycache__ when the compiled path is
355358
given.
356359

0 commit comments

Comments
 (0)