Reputation: 136161
I have a function that gets a list of DB tables as parameter, and returns a command string to be executed on these tables, e.g.:
pg_dump( file='/tmp/dump.sql',
tables=('stack', 'overflow'),
port=5434
name=europe)
Should return something like:
pg_dump -t stack -t overflow -f /tmp/dump.sql -p 5434 europe
This is done using tables_string='-t '+' -t '.join(tables)
.
The fun begins when the function is called with: tables=('stackoverflow')
(a string) instead of tables=('stackoverflow',)
(a tuple), which yields:
pg_dump -t s -t t -t a -t c -t k -t o -t v -t e -t r -t f -t l -t o -t w
-f /tmp/dump.sql -p 5434 europe
Because the string itself is being iterated.
This SO question suggests using asserts on the type, but I'm not sure it's Pythonic enough because it breaks the duck-type convention.
Any insights?
Adam
Upvotes: 7
Views: 1294
Reputation: 71
The cleanest way I can think of is to create a new Abstract Collection type "NonStrSequence" by overriding subclasshook. See below implementation and tests:
from typing import Sequence, ByteString
from abc import ABC
class NonStrSequence(ABC):
@classmethod
def __subclasshook__(cls, C):
# not possible to do with AnyStr
if issubclass(C, (str, ByteString)):
return NotImplemented
else:
return issubclass(C, Sequence)
tests = {
'list_of_strs': ['b', 'c'],
'str': 'abc',
'bytes': b'bytes',
'tuple': ([1,2], 'a'),
'str_in_parens': ('a'), # Not a tuple
'str_in_tuple': ('a',),
}
for type in [Sequence, NonStrSequence]:
for k,v in tests.items():
print(f'{k}: isinstance({v}, {type}): {isinstance(v, type)}')
Upvotes: 0
Reputation: 123632
You can use ABCs to assert that an object is iterable but not a string:
from types import StringType
from collections import Iterable
assert isinstance(x, Iterable) and not isinstance(x, StringType)
Upvotes: 3
Reputation: 123443
A common Python idiom to detect whether an argument is a sequence (a list or tuple) or a string is to check whether it has the __iter__
attribute:
def func(arg):
if hasattr(arg, '__iter__'):
print repr(arg), 'has __iter__ attribute'
else:
print repr(arg), 'has no __iter__ attribute'
func('abc')
# 'abc' has no __iter__
func(('abc'))
# 'abc' has no __iter__
func(('abc',))
# ('abc',) has __iter__
When it's not a sequence, it's also common to change it into one to simplify the rest of the code (which only has to deal with one kind of thing). In the sample it could have been done with a simple arg = [arg]
.
Upvotes: 1
Reputation: 75427
Asserting the type seems appropriate in this case - handling a common misuse that seems legal because of duck typing.
Another way to handle this common case would be to test for string and handle it correctly as a special case.
Finally, you could encourage passing the table names as positional parameters which would make this scenario less likely:
def pg_dump(*tables, **kwargs):
file = kwargs['file']
port = kwargs['port']
name = kwargs['name']
...
pg_dump('stack', 'overflow', file='/tmp/dump.sql', port=5434, name='europe')
Upvotes: 6
Reputation: 4046
Can you not use a list rather than a tuple?
pg_dump( file='/tmp/dump.sql',
tables=['stack', 'overflow'],
port=5434,
name='europe')
Upvotes: 0