Merge lp:~gerdusvanzyl/storm/firebird into lp:storm

Proposed by Gerdus van Zyl on 2010-06-26
Status: Needs review
Proposed branch: lp:~gerdusvanzyl/storm/firebird
Merge into: lp:storm
Diff against target: 587 lines (+427/-27)
4 files modified
storm/databases/firebird.py (+229/-0)
tests/databases/base.py (+25/-25)
tests/databases/firebird.py (+171/-0)
tests/databases/proxy.py (+2/-2)
To merge this branch: bzr merge lp:~gerdusvanzyl/storm/firebird
Reviewer Review Type Date Requested Status
Storm Developers 2010-08-02 Pending
Storm Developers 2010-06-26 Pending
Review via email: mp+28559@code.launchpad.net

Description of the change

Basic Firebird database support.

Passes all tests except "Connection.rollback() swallows disconnect errors" which I have so far been unable to fix.

example connection string env variable for tests: STORM_FIREBIRD_URI=firebird://sysdba:masterke@localhost:3050/stormtest.fdb

To post a comment you must log in.
Gustavo Niemeyer (niemeyer) wrote :

Hi Gerdus,

Thanks for pushing this forward into a request!

Sorry if this has been asked already, but if not, can you please sign the contributor agreement at:

    http://www.canonical.com/contributors

Gerdus van Zyl (gerdusvanzyl) wrote :

No problem, I have just sent the email as per the instructions.

On Tue, Aug 10, 2010 at 7:23 PM, Gustavo Niemeyer <email address hidden> wrote:
> Hi Gerdus,
>
> Thanks for pushing this forward into a request!
>
> Sorry if this has been asked already, but if not, can you please sign the contributor agreement at:
>
>    http://www.canonical.com/contributors
> --
> https://code.launchpad.net/~gerdusvanzyl/storm/firebird/+merge/28559
> You are the owner of lp:~gerdusvanzyl/storm/firebird.
>

--
Gerdus van Zyl

Unmerged revisions

368. By Gerdus van Zyl on 2010-06-16

merge from trunk

367. By Gerdus van Zyl on 2010-06-16

cleanup unused comments

366. By Gerdus van Zyl on 2010-06-16

fixed proxy to work on windows
replaced os.read(self.request.fileno() .. with self.request.recv

365. By Gerdus van Zyl on 2010-06-16

-

364. By Gerdus van Zyl on 2010-06-16

disconnect cleanup, push to vpc

363. By Gerdus van Zyl on 2010-06-16

is_disconnection_error

362. By Gerdus van Zyl on 2010-06-16

test cases for returning and select limit

361. By Gerdus van Zyl on 2010-06-13

start of disconnect test, fix base database test to not use "select 1"

360. By Gerdus van Zyl on 2010-06-13

cleanup and override non implementable tests

359. By Gerdus van Zyl on 2010-06-06

code to pass test test_execute_expression and test_execute_expression_empty_params

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'storm/databases/firebird.py'
2--- storm/databases/firebird.py 1970-01-01 00:00:00 +0000
3+++ storm/databases/firebird.py 2010-06-26 13:26:23 +0000
4@@ -0,0 +1,229 @@
5+#
6+# Copyright (c) 2010 Canonical
7+#
8+# Initial Code by Gerdus van Zyl
9+#
10+# This file is part of Storm Object Relational Mapper.
11+#
12+#
13+# Storm is free software; you can redistribute it and/or modify
14+# it under the terms of the GNU Lesser General Public License as
15+# published by the Free Software Foundation; either version 2.1 of
16+# the License, or (at your option) any later version.
17+#
18+# Storm is distributed in the hope that it will be useful,
19+# but WITHOUT ANY WARRANTY; without even the implied warranty of
20+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21+# GNU Lesser General Public License for more details.
22+#
23+# You should have received a copy of the GNU Lesser General Public License
24+# along with this program. If not, see <http://www.gnu.org/licenses/>.
25+#
26+#
27+"""
28+Tested only with: Kinterbase 3.3 and Firebird 2.1
29+"""
30+
31+import os.path
32+from datetime import timedelta
33+from storm.databases import dummy
34+
35+from storm.expr import (compile, Select, compile_select, Undef,
36+ Expr, Insert,COLUMN,Sequence)
37+from storm.variables import (Variable,IntVariable)
38+from storm.database import Database, Connection, Result
39+
40+from storm.exceptions import (
41+ install_exceptions, DatabaseError, DatabaseModuleError, InterfaceError,
42+ OperationalError, ProgrammingError, TimeoutError)
43+
44+try:
45+ import kinterbasdb
46+ install_exceptions(kinterbasdb)
47+except ImportError:
48+ kinterbasdb = dummy
49+
50+try:
51+ kinterbasdb.init(type_conv=200)
52+except kinterbasdb.ProgrammingError:
53+ pass #Cannot initialize module more than once.
54+
55+compile = compile.create_child()
56+
57+@compile.when(int, long)
58+def compile_int(compile, expr, state):
59+ state.parameters.append(IntVariable(expr))
60+ return "cast(? as integer)"
61+
62+@compile.when(Select)
63+def compile_select_firebird(compile, select, state):
64+ limit = select.limit
65+ offset = select.offset
66+ # Make sure limit is Undef'ed.
67+ select.offset = select.limit = Undef
68+
69+ if select.default_tables is Undef:
70+ select.default_tables = ["RDB$DATABASE"]
71+
72+ sql = compile_select(compile, select, state)
73+
74+ if limit is not Undef or offset is not Undef:
75+ rowstart = 1
76+ rowstop = None
77+ if offset is not Undef:
78+ rowstart = offset + 1
79+ if limit is not Undef:
80+ rowstop = rowstart + limit - 1
81+ if rowstop < rowstart:
82+ rowstop = rowstart
83+
84+ sql += " ROWS %i" % rowstart
85+ if rowstop:
86+ sql += " TO %i " % rowstop
87+ #print sql
88+
89+ return sql
90+
91+@compile.when(Sequence)
92+def compile_sequence_firebird(compile, sequence, state):
93+ return "gen_id(%s, 1)" % sequence.name
94+
95+class Returning(Expr):
96+ """Appends the "RETURNING <primary_columns>" suffix to an INSERT.
97+
98+ This is only supported in Firebird 2.0
99+ """
100+ def __init__(self, insert):
101+ self.insert = insert
102+
103+@compile.when(Returning)
104+def compile_returning(compile, expr, state):
105+ state.push("context", COLUMN)
106+ columns = compile(expr.insert.primary_columns, state)
107+ state.pop()
108+ state.push("precedence", 0)
109+ insert = compile(expr.insert, state)
110+ state.pop()
111+ return "%s RETURNING %s" % (insert, columns)
112+
113+class FirebirdResult(Result):
114+ @staticmethod
115+ def from_database(row):
116+ """Convert Firebird-specific datatypes to "normal" Python types.
117+
118+ If there are any C{array} instances in the row, convert them
119+ to strings.
120+ """
121+ for value in row:
122+ yield value
123+
124+ def get_insert_identity(self, primary_key, primary_variables):
125+ """
126+ Firebird does not support insert identity
127+ - autoinc is implemented using custom triggers
128+ so no clear way to support it in a generic way
129+ """
130+ raise NotImplementedError
131+
132+class FirebirdConnection(Connection):
133+
134+ result_factory = FirebirdResult
135+ param_mark = "?"
136+ compile = compile
137+ server_version = None
138+
139+ def execute(self, statement, params=None, noresult=False):
140+ """Execute a statement with the given parameters.
141+
142+ This extends the L{Connection.execute} method to add support
143+ for automatic retrieval of inserted primary keys to link
144+ in-memory objects with their specific rows.
145+ """
146+ if not self.server_version:
147+ version = 0
148+ version = self._raw_connection.db_info(kinterbasdb.isc_info_version)
149+ version = str(version).split("Firebird")[1].strip()
150+ version = float(version)
151+ self.server_version = version
152+
153+ if (isinstance(statement, Insert) and
154+ self.server_version >= 2 and
155+ statement.primary_variables is not Undef and
156+ statement.primary_columns is not Undef):
157+
158+ # Here we decorate the Insert statement with a Returning
159+ # expression, so that we get back in the result the values
160+ # for the primary key just inserted. This prevents a round
161+ # trip to the database for obtaining these values.
162+ result = Connection.execute(self, Returning(statement), params)
163+ for variable, value in zip(statement.primary_variables,
164+ result.get_one()):
165+ result.set_variable(variable, value)
166+ return result
167+
168+ return Connection.execute(self, statement, params, noresult)
169+
170+ def is_disconnection_error(self, exc):
171+ """Check whether an exception represents a database disconnection.
172+
173+ """
174+ if isinstance(exc, ProgrammingError) or isinstance(exc,OperationalError):
175+ code,description = exc.args
176+ if code == -902:
177+ return True
178+
179+
180+ return False
181+
182+ def to_database(self, params):
183+ for param in params:
184+ if isinstance(param, Variable):
185+ param = param.get(to_db=True)
186+ if isinstance(param, timedelta):
187+ yield str(param)
188+ else:
189+ yield param
190+
191+class Firebird(Database):
192+
193+ connection_factory = FirebirdConnection
194+ _converters = None
195+
196+ def __init__(self, uri):
197+ if kinterbasdb is dummy:
198+ raise DatabaseModuleError("'kinterbasdb' module not found")
199+ self._connect_kwargs = {}
200+ if uri.database is not None:
201+ if os.path.isfile(uri.database):
202+ uri.database = os.path.abspath(uri.database)
203+ self._connect_kwargs["database"] = uri.database
204+ if uri.host is not None:
205+ self._connect_kwargs["host"] = uri.host
206+ if uri.port is not None:
207+ #firebird expects nonstandard port spec: http://www.firebirdfaq.org/faq259/
208+ self._connect_kwargs["host"] = "%s/%s" % (uri.host,uri.port)
209+ if uri.port is not None:
210+ self._connect_kwargs["port"] = uri.port
211+ if uri.username is not None:
212+ self._connect_kwargs["user"] = uri.username
213+ if uri.password is not None:
214+ self._connect_kwargs["password"] = uri.password
215+ for option in ["unix_socket"]:
216+ if option in uri.options:
217+ self._connect_kwargs[option] = uri.options.get(option)
218+
219+
220+
221+ def raw_connect(self):
222+ customTPB = (
223+ kinterbasdb.isc_tpb_write
224+ + kinterbasdb.isc_tpb_concurrency
225+ )
226+
227+ raw_connection = kinterbasdb.connect(**self._connect_kwargs)
228+ raw_connection.default_tpb = customTPB
229+
230+ return raw_connection
231+
232+
233+create_from_uri = Firebird
234
235=== modified file 'tests/databases/base.py'
236--- tests/databases/base.py 2010-04-16 07:12:13 +0000
237+++ tests/databases/base.py 2010-06-26 13:26:23 +0000
238@@ -132,7 +132,7 @@
239 self.assertTrue(result.get_one())
240
241 def test_execute_result(self):
242- result = self.connection.execute("SELECT 1")
243+ result = self.connection.execute("SELECT 1 FROM TEST")
244 self.assertTrue(isinstance(result, Result))
245 self.assertTrue(result.get_one())
246
247@@ -357,7 +357,7 @@
248 event.hook("register-transaction", register_transaction)
249
250 connection = self.database.connect(event)
251- connection.execute("SELECT 1")
252+ connection.execute("SELECT 1 FROM TEST")
253 self.assertEqual(len(calls), 1)
254 self.assertEqual(calls[0], marker)
255
256@@ -425,16 +425,16 @@
257
258 def test_block_access(self):
259 """Access to the connection is blocked by block_access()."""
260- self.connection.execute("SELECT 1")
261+ self.connection.execute("SELECT 1 FROM TEST")
262 self.connection.block_access()
263 self.assertRaises(ConnectionBlockedError,
264- self.connection.execute, "SELECT 1")
265+ self.connection.execute, "SELECT 1 FROM TEST")
266 self.assertRaises(ConnectionBlockedError, self.connection.commit)
267 # Allow rolling back a blocked connection.
268 self.connection.rollback()
269 # Unblock the connection, allowing access again.
270 self.connection.unblock_access()
271- self.connection.execute("SELECT 1")
272+ self.connection.execute("SELECT 1 FROM TEST")
273
274
275 class UnsupportedDatabaseTest(object):
276@@ -556,20 +556,20 @@
277
278 def test_proxy_works(self):
279 """Ensure that we can talk to the database through the proxy."""
280- result = self.connection.execute("SELECT 1")
281+ result = self.connection.execute(Select(1))
282 self.assertEqual(result.get_one(), (1,))
283
284 def test_catch_disconnect_on_execute(self):
285 """Test that database disconnections get caught on execute()."""
286- result = self.connection.execute("SELECT 1")
287+ result = self.connection.execute(Select(1))
288 self.assertTrue(result.get_one())
289 self.proxy.restart()
290 self.assertRaises(DisconnectionError,
291- self.connection.execute, "SELECT 1")
292+ self.connection.execute, Select(1))
293
294 def test_catch_disconnect_on_commit(self):
295 """Test that database disconnections get caught on commit()."""
296- result = self.connection.execute("SELECT 1")
297+ result = self.connection.execute(Select(1))
298 self.assertTrue(result.get_one())
299 self.proxy.restart()
300 self.assertRaises(DisconnectionError, self.connection.commit)
301@@ -581,13 +581,13 @@
302 then it is possible that Storm won't see the disconnection.
303 It should be able to recover from this situation though.
304 """
305- result = self.connection.execute("SELECT 1")
306+ result = self.connection.execute(Select(1))
307 self.assertTrue(result.get_one())
308 self.proxy.restart()
309 # Perform an action that should result in a disconnection.
310 try:
311 cursor = self.connection._raw_connection.cursor()
312- cursor.execute("SELECT 1")
313+ cursor.execute("select 1 from test")
314 cursor.fetchone()
315 except Error, exc:
316 self.assertTrue(self.connection.is_disconnection_error(exc))
317@@ -614,59 +614,59 @@
318 then it is possible that Storm won't see the disconnection.
319 It should be able to recover from this situation though.
320 """
321- result = self.connection.execute("SELECT 1")
322+ result = self.connection.execute(Select(1))
323 self.assertTrue(result.get_one())
324 self.proxy.restart()
325 # Perform an action that should result in a disconnection.
326 try:
327 cursor = self.connection._raw_connection.cursor()
328- cursor.execute("SELECT 1")
329+ cursor.execute("SELECT 1 FROM TEST")
330 cursor.fetchone()
331 except DatabaseError, exc:
332 self.assertTrue(self.connection.is_disconnection_error(exc))
333 else:
334 self.fail("Disconnection was not caught.")
335 self.assertRaises(DisconnectionError,
336- self.connection.execute, "SELECT 1")
337+ self.connection.execute, Select(1))
338
339 def test_connection_stays_disconnected_in_transaction(self):
340 """Test that connection does not immediately reconnect."""
341- result = self.connection.execute("SELECT 1")
342+ result = self.connection.execute(Select(1))
343 self.assertTrue(result.get_one())
344 self.proxy.restart()
345 self.assertRaises(DisconnectionError,
346- self.connection.execute, "SELECT 1")
347+ self.connection.execute, Select(1))
348 self.assertRaises(DisconnectionError,
349- self.connection.execute, "SELECT 1")
350+ self.connection.execute, Select(1))
351
352 def test_reconnect_after_rollback(self):
353 """Test that we reconnect after rolling back the connection."""
354- result = self.connection.execute("SELECT 1")
355+ result = self.connection.execute(Select(1))
356 self.assertTrue(result.get_one())
357 self.proxy.restart()
358 self.assertRaises(DisconnectionError,
359- self.connection.execute, "SELECT 1")
360+ self.connection.execute, Select(1))
361 self.connection.rollback()
362- result = self.connection.execute("SELECT 1")
363+ result = self.connection.execute(Select(1))
364 self.assertTrue(result.get_one())
365
366 def test_catch_disconnect_on_reconnect(self):
367 """Test that reconnection failures result in DisconnectionError."""
368- result = self.connection.execute("SELECT 1")
369+ result = self.connection.execute(Select(1))
370 self.assertTrue(result.get_one())
371 self.proxy.stop()
372 self.assertRaises(DisconnectionError,
373- self.connection.execute, "SELECT 1")
374+ self.connection.execute, Select(1))
375 # Rollback the connection, but because the proxy is still
376 # down, we get a DisconnectionError again.
377 self.connection.rollback()
378 self.assertRaises(DisconnectionError,
379- self.connection.execute, "SELECT 1")
380+ self.connection.execute, Select(1))
381
382 def test_close_connection_after_disconnect(self):
383- result = self.connection.execute("SELECT 1")
384+ result = self.connection.execute(Select(1))
385 self.assertTrue(result.get_one())
386 self.proxy.stop()
387 self.assertRaises(DisconnectionError,
388- self.connection.execute, "SELECT 1")
389+ self.connection.execute, Select(1))
390 self.connection.close()
391
392=== added file 'tests/databases/firebird.py'
393--- tests/databases/firebird.py 1970-01-01 00:00:00 +0000
394+++ tests/databases/firebird.py 2010-06-26 13:26:23 +0000
395@@ -0,0 +1,171 @@
396+#
397+# Copyright (c) 2010 Canonical
398+#
399+# Initial Code by Gerdus van Zyl
400+#
401+# This file is part of Storm Object Relational Mapper.
402+#
403+# Storm is free software; you can redistribute it and/or modify
404+# it under the terms of the GNU Lesser General Public License as
405+# published by the Free Software Foundation; either version 2.1 of
406+# the License, or (at your option) any later version.
407+#
408+# Storm is distributed in the hope that it will be useful,
409+# but WITHOUT ANY WARRANTY; without even the implied warranty of
410+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
411+# GNU Lesser General Public License for more details.
412+#
413+# You should have received a copy of the GNU Lesser General Public License
414+# along with this program. If not, see <http://www.gnu.org/licenses/>.
415+#
416+import os
417+
418+from storm.database import create_database
419+from storm.database import *
420+
421+from tests.helper import TestHelper
422+
423+from tests.databases.base import (
424+ DatabaseTest, UnsupportedDatabaseTest, DatabaseDisconnectionTest)
425+
426+from storm.variables import (
427+ IntVariable, Variable )
428+
429+from storm.exceptions import (
430+ DatabaseError, DatabaseModuleError,
431+ DisconnectionError, Error, OperationalError, ConnectionBlockedError)
432+
433+from tests.expr import (
434+ Select,Column,Insert, column1, column2, column3, elem1,
435+ table1, TrackContext,Sequence)
436+
437+#import storm.tracer
438+#import sys
439+#storm.tracer.debug(True, stream=sys.stdout)
440+
441+class FirebirdTest(DatabaseTest, TestHelper):
442+ supports_microseconds = False
443+
444+ def is_supported(self):
445+ return bool(os.environ.get("STORM_FIREBIRD_URI"))
446+
447+ def create_database(self):
448+ self.database = create_database(os.environ["STORM_FIREBIRD_URI"])
449+
450+ def create_tables(self):
451+ self.connection.execute('CREATE TABLE NUMBER (one INTEGER, two INTEGER, three INTEGER)')
452+ self.connection.execute('CREATE TABLE TEST (id INTEGER PRIMARY KEY, title VARCHAR(50) CHARACTER SET UTF8)')
453+ self.connection.execute('CREATE TABLE DATETIME_TEST (id INT UNIQUE,dt TIMESTAMP, d DATE, t TIME, td BLOB SUB_TYPE TEXT)')
454+
455+ self.connection.execute('CREATE TABLE BIN_TEST (id INT UNIQUE,b BLOB)')
456+
457+ self.connection.execute('CREATE GENERATOR GEN_TEST_AUTOID')
458+ self.connection.execute('SET GENERATOR GEN_TEST_AUTOID TO 0')
459+
460+ self.connection.execute("""CREATE TRIGGER TEST_PK_AUTO FOR TEST ACTIVE BEFORE INSERT POSITION 0 AS
461+begin
462+ if ( (new.ID is null) or (new.ID = 0) )
463+ then new.ID = gen_id(GEN_TEST_AUTOID, 1);
464+end""")
465+
466+ self.connection.execute("""CREATE TABLE insert_returning_test
467+ (id0 INTEGER,
468+ id1 INTEGER DEFAULT 123,
469+ id2 INTEGER DEFAULT 456)""")
470+
471+ self.connection.commit()
472+
473+ def drop_tables(self):
474+ try:
475+ self.connection.execute("DROP TRIGGER TEST_PK_AUTO")
476+ self.connection.execute("DROP GENERATOR GEN_TEST_AUTOID")
477+ self.connection.commit()
478+ except:
479+ self.connection.rollback()
480+
481+ for table in ["number", "test", "datetime_test", "bin_test", "insert_returning_test"]:
482+ try:
483+ self.connection.execute("DROP TABLE " + table)
484+ self.connection.commit()
485+ except:
486+ self.connection.rollback()
487+
488+ def test_get_insert_identity(self):
489+ """test_get_insert_identity -Does not support insert identity"""
490+ #http://www.firebirdfaq.org/faq243/
491+ pass
492+
493+ def test_get_insert_identity_composed(self):
494+ """test_get_insert_identity_composed - Does not support insert identity"""
495+ #http://www.firebirdfaq.org/faq243/
496+ pass
497+
498+ def test_execute_insert_returning(self):
499+ if self.connection.server_version < 2:
500+ return # Can't run this test with old PostgreSQL versions.
501+
502+ column0 = Column("id0", "insert_returning_test")
503+ column1 = Column("id1", "insert_returning_test")
504+ column2 = Column("id2", "insert_returning_test")
505+ variable1 = IntVariable()
506+ variable2 = IntVariable()
507+ insert = Insert({column0: 999}, primary_columns=(column1, column2),
508+ primary_variables=(variable1, variable2))
509+ self.connection.execute(insert)
510+
511+ self.assertTrue(variable1.is_defined())
512+ self.assertTrue(variable2.is_defined())
513+
514+ self.assertEquals(variable1.get(), 123)
515+ self.assertEquals(variable2.get(), 456)
516+
517+ result = self.connection.execute("SELECT * FROM insert_returning_test")
518+ self.assertEquals(result.get_one(), (999,123, 456))
519+
520+ def test_sequence(self):
521+ expr1 = Select(Sequence("GEN_TEST_AUTOID"))
522+ expr2 = "SELECT gen_id(GEN_TEST_AUTOID,0) FROM RDB$DATABASE"
523+ value1 = self.connection.execute(expr1).get_one()[0]
524+ value2 = self.connection.execute(expr2).get_one()[0]
525+ value3 = self.connection.execute(expr1).get_one()[0]
526+ self.assertEquals(value1, value2)
527+ self.assertEquals(value3-value1, 1)
528+
529+ def test_limit_offset(self):
530+ self.connection.execute("delete from test")
531+ self.connection.commit()
532+
533+ for z in range(100,200+1):
534+ sql = "INSERT INTO test (id,title) VALUES (%i,'%i')" % (z,z)
535+ self.connection.execute(sql)
536+ self.connection.commit()
537+
538+ select = Select(Column("id", "test"))
539+ select.limit = 1
540+ select.offset = 0
541+ select.order_by = Column("id", "test")
542+
543+ result = self.connection.execute(select)
544+ self.assertEquals(result.get_all(), [(100,),] )
545+
546+
547+ select = Select(Column("id", "test"))
548+ select.limit = 2
549+ select.offset = 50
550+ select.order_by = Column("id", "test")
551+
552+ result = self.connection.execute(select)
553+ self.assertEquals(result.get_all(), [(150,),(151,)] )
554+
555+
556+class FirebirdUnsupportedTest(UnsupportedDatabaseTest, TestHelper):
557+
558+ dbapi_module_names = ["kinterbasdb"]
559+ db_module_name = "firebird"
560+
561+
562+class FirebirdDisconnectionTest(DatabaseDisconnectionTest, TestHelper):
563+
564+ environment_variable = "STORM_FIREBIRD_URI"
565+ host_environment_variable = "STORM_FIREBIRD_HOST_URI"
566+ default_port = 3050
567
568=== modified file 'tests/databases/proxy.py'
569--- tests/databases/proxy.py 2007-10-24 06:27:06 +0000
570+++ tests/databases/proxy.py 2010-06-26 13:26:23 +0000
571@@ -52,14 +52,14 @@
572 return
573
574 if self.request in rlist:
575- chunk = os.read(self.request.fileno(), 1024)
576+ chunk = self.request.recv(1024)
577 dst.send(chunk)
578 if chunk == "":
579 readers.remove(self.request)
580 dst.shutdown(socket.SHUT_WR)
581
582 if dst in rlist:
583- chunk = os.read(dst.fileno(), 1024)
584+ chunk = dst.recv(1024)
585 self.request.send(chunk)
586 if chunk == "":
587 readers.remove(dst)

Subscribers

People subscribed via source and target branches

to status/vote changes: