17

I have several possible files which could hold my data; they can be compressed in different ways, so to open them I need to use file(), gzip.GzipFile() and other which also return a file object (supporting the with interface).

I want to try each of them until one succeeds in opening, so I could do something like

try:
  with gzip.GzipFile(fn + '.gz') as f:
    result = process(f)
except (IOError, MaybeSomeGzipExceptions):
  try:
    with xCompressLib.xCompressFile(fn + '.x') as f:
      result = process(f)
  except (IOError, MaybeSomeXCompressExceptions):
    try:
      with file(fn) as f:
        result = process(f)
    except IOError:
      result = "some default value"

which obviously isn't feasible in case I have dozens of possible compression variants. (The nesting will get deeper and deeper, the code always looking very much alike.)

Is there a nicer way to spell this out?

EDIT: If possible I'd like to have the process(f) out of the try/except as well to avoid accidental catching of exceptions raised in the process(f).

Vicent
  • 5,322
  • 2
  • 28
  • 36
Alfe
  • 56,346
  • 20
  • 107
  • 159

3 Answers3

9

Yea, you could put all your variants through a list and try them until one of them works, thus un-nesting your code:

def process_gzip(fn):
    with gzip.GzipFile(fn + '.gz') as f:
        return process(f)

def process_xlib(fn):
    with xCompressLib.xCompressFile(fn + '.x') as f:
        return process(f)

def process_builtin(fn):
    with file(fn) as f:
        return process(f)

process_funcs = [process_gzip, process_xlib, process_builtin]

#processing code:

for process_f in process_funcs:
    try:
        result = process_f(fn)
        break
    except IOError:
        #error reading the file, keep going
        continue
    except:
        #processing error, re-raise the exception
        raise

Or, to reduce amount of code you could make a process_func factory, since they all have the same form:

def make_process_func(constructor, filename_transform):
    with constructor(filename_transform) as f:
        return process(f)

process_funcs = [
    make_process_func(gzip.GzipFile, lambda fn: fn + '.gz'),
    make_process_func(xCompressLib.xCompressFile, lambda fn: fn + '.x'),
    make_process_func(file, lambda fn: fn),
]
Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • I have an EDIT to my question: I'd like to have the `process(f)` out of the try/except if possible. – Alfe Sep 24 '12 at 12:47
  • 1
    just opening the file might not cause an IOError. reading the file might do so as well. so you'll want to read all the data out and then process it if you want to separate IOError from processing error, no? – Claudiu Sep 24 '12 at 12:47
  • 1
    The only thing that I would add here is that you should probably keep tuples on hand which contain the various acceptable exceptions. e.g. `try: ; except passable_exceptions: pass` – mgilson Sep 24 '12 at 12:48
  • Ahh! you beat me by a couple of minutes. Same approach. +1! – inspectorG4dget Sep 24 '12 at 12:49
  • mglison is right, I'd like to have different acceptable Exceptions. – Alfe Sep 24 '12 at 12:55
7

I'd write a custom context manager:

from contextlib import contextmanager

filetypes = [('.gz', gzip.GzipFile, (IOError, MaybeSomeGzipExceptions)), 
             ('.x', xCompressLib.xCompressFile, (IOError, MaybeSomeXCompressExceptions))]

@contextmanager
def open_compressed(fn):
    f = None
    try:
        for ext, cls, exs in filetypes:
            try:
                f = cls(fn + ext)
            except exs:
                pass
            else:
                break
        yield f
    finally:
        if f is not None:
            f.close()

with open_compressed(fn) as f:
    result = "some default value" if f is None else process(f)

Or possibly just a function that returns a context manager:

filetypes = [('.gz', gzip.GzipFile, (IOError, MaybeSomeGzipExceptions)), 
             ('.x', xCompressLib.xCompressFile, (IOError, MaybeSomeXCompressExceptions))]

class UnknownCompressionFormat(Exception):
    pass

def open_compressed(fn):
    for ext, cls, exs in filetypes:
        try:
            return cls(fn + ext)
        except exs:
            pass
    raise UnknownCompressionFormat

try:
    with open_compressed(fn) as f:
        result = process(f)
except UnknownCompressionFormat:
    result = "some default value"
ecatmur
  • 152,476
  • 27
  • 293
  • 366
5

Would this work:

extensions = [('.gz', gzip.GzipFile, (IOError, MaybeSomeGzipExceptions)), 
              ('.x', xCompressLib.xCompressFile, (IOError, MaybeSomeXCompressExceptions))] # and other such entries
processed = False
for ext, (compressor, errors) in extensions.iteritems():
    try:
        with compressor(fn+ext) as f:
            try:
                result = process(f)
                processed = True
                break
            except:
                raise
    except errors:
        pass
if not processed:
    result = "some default value"

Hope that helps

inspectorG4dget
  • 110,290
  • 27
  • 149
  • 241
  • Again, I have an EDIT to my question: is it possible to get the `process(f)` out of the try/catch (which should only catch exceptions in the `with` stuff anyway)? – Alfe Sep 24 '12 at 12:49
  • This is what I would do too, but it's possible that order might matter. – mgilson Sep 24 '12 at 12:49
  • Order matters indeed, but using a list of tuples instead of a dict is no problem. – Alfe Sep 24 '12 at 12:52
  • A list of `tuples` is what I would have suggested. As to your other question, what does `process` actually *do*? Does it read from the file? If so, you can't get it out of `with`, and therefore, you can't get it out of the exception handler. (Indeed, the exceptions might not be raised until you try to read from the file). You can read the data and then pass the *data* to `process` if it isn't too big. Otherwise, you need to make sure that `process` doesn't raise the same exceptions that the filetypes might raise (which shouldn't be too hard). – mgilson Sep 24 '12 at 12:56
  • Of course it's using the file (to read or write). In fact in my current case I'm pickling data. – Alfe Sep 24 '12 at 13:03
  • Hmmm.. I still have a problem with the following fact: If the file exists (`compressor(fn+ext)` raises no exception) but then contains nonsense (reading it raises an exception), I'd rather not catch that exception (or at least re-raise it). In this implementation I would not recognize this fact and, consequently, would try the next compression algorithm. That's why I wanted to have the opening separately from the reading. Any idea? – Alfe Sep 24 '12 at 13:11
  • The problem then is, in the end I end up with the default value instead of some information stating that there is nonsense in an existing file. – Alfe Sep 24 '12 at 13:12
  • The `process()` still might raise exceptions of the classes you catch in the outer try/except, thus these then go unnoticed (especially for compression classes which for everything (opening, processing, ...) have just one exception, so I cannot distinguish between the cause; except by the place they happen). – Alfe Sep 24 '12 at 13:28
  • You *could* catch such an exception in the inner part, wrap any caught one in a special exception, raise that, so it doesn't get caught in the outer try/catch, catch it again even more outwards, unwrap it and re-raise the original. But that's not what I'd call nice and readable. I think I will step down in my case and rather use `os.path.exists()` before trying to open those beasts. Thanks for helping me find my way! :) – Alfe Sep 24 '12 at 13:31