python to ruby translation no. 1

16 Jul 2014

Yesterday, I was browsing through the Dropbox docs, developer forum and blog, trying to find a method or endpoint that returns multiple file results at once (not just the file path or name, but the actual file). I wasn't able to find what I was looking for, but in the process, I came across one of their dev blog posts about delta(). It contained a lot of information, some of it potentially useful to me, but at also had examples in Python.

I had seen Python before, but I'd never taken a good look. Now here I was, faced with some pieces of code that might yield some nuggets of gold -- if I wanted to decipher the example.1 Honestly, I was excited by the prospect. This was a chance to learn some Python, maybe learn more about Ruby in the process, and do some fun translation work.

I started with this chunk of Python and started my Ruby interpretation:

def list_files(client, files=None, cursor=None):
    if files is None:
        files = {}

    has_more = True

    while has_more:
        result = client.delta(cursor)
        cursor = result['cursor']
        has_more = result['has_more']

        for lowercase_path, metadata in result['entries']:

            if metadata is not None:
                files[lowercase_path] = metadata

            else:
                # no metadata indicates a deletion

                # remove if present
                files.pop(lowercase_path, None)

                # in case this was a directory, delete everything under it
                for other in files.keys():
                    if other.startswith(lowercase_path + '/'):
                        del files[other]

    return files, cursor

The most obvious thing I noticed was the use of white space to close each code block. So my first step was to go in and add 'end' as necessary. Then I started to identify things that looked familiar to me:

  • Accepting arguments when defining a method and assigning defaults to them in the first line
  • Setting up an empty hash with just files = {} and subesquently getting values from the hash with the square brackets syntax, e.g. result['cursor']
  • A while loop and for loops
  • if / else statements
  • Simple variable assigment: has_more = True
  • Boolean value of True just a capitalized version of Ruby's true

Then were the things that were slightly different but I could understand by analogy:

  • None: I immediately assumed this was the rough equivalent to Ruby's nil, but did a quick Google search to confirm. Yes, both None and nil are singleton objects, and appear to be used in similar ways.2
  • is None apparent equivalent of nil? and is not None equivalent of !nil?
  • Beginning a block with : corresponds with Ruby's do
  • .startswith() matches Ruby's .start_with?()

That pretty much left only two things that had yet to be translated: files.pop(lowercase_path, None) and del files[other]. pop looked dangerously similar to Ruby's Array instance method #pop, but since this was being called on a hash I suspected it wasn't quite the same. After some investigation (i.e. a Google search for "python hash pop"), I found out a couple new things -- first, files wasn't exactly a hash as I know it, but this thing called a dictionary. dict.pop(key[, default]) is function you can call on a dictionary (like a Ruby hash) to remove a key that exists (and matches the first argument) and return its value; and if the key doesn't exist, return the default supplied as the second argument. If the key doesn't exist and you don't provide a default value as the second argument, then a KeyError is raised.

This function is similar to Ruby's Hash#delete(key), but delete(key) assumes more than dict.pop(key[, default]) does -- if the key doesn't exist, it automatically returns nil or the already given default value of the hash,4 instead of raising an error or exception. So here, files.pop(lowercase_path, None) was the equivalent of files.delete(lowercase_path).

That just left del files[other], which apparently is also the rough equivalent of Ruby's files.delete[other], with one noticeable difference being that Python's del statement doesn't return anything, whereas Ruby's method will return the value of other or the hash's default.5 That was pretty interesting to notice -- using this awesome in-browser interpreter to try out this Python business,6 I noticed that some functions in Python just don't return a value back to you, whereas Ruby methods always do.7

Here's my Ruby version of the above. It's pretty literal; I didn't break it out into smaller methods or objects at all, and I didn't try to pay too much attention to idioms because I don't know anything about Python idioms yet.


def list_files(client, files=nil, cursor=nil)
  if files.nil?
    files = {}
  end

  has_more = true

  while has_more do
    result = client.delta(cursor)
    cursor = result['cursor']
    has_more = result['has_more']

    for lowercase_path, metadata in result['entries'] do

      if metadata  # literally, if !metadata.nil?
        files[lowercase_path] = metadata

      else
      # no metadata indicates a deletion

      # remove if present
        files.delete(lowercase_path)

        # in case this was a directory, delete everything under it
        for other in files.keys() do
          if other.start_with?(lowercase_path + '/')
            files.delete(other)
          end
        end
      end
    end
  end
  return files, cursor
end


1 Yeah, yeah, I know for most people the blog post text itself would've been enough, but if my use case matched this one, I wanted to know exactly what the example was doing.
4 In Ruby, you can predefine the default of a hash when you initialize it, by simply passing it as an argument: Hash.new(default). In some ways, this is super useful, but it also requires you to remember what you set as a default, which could get hairy.
5 The answer and its comments for this SO post are also interesting and hint at some differences between Python's dictionary and Ruby's hash that are definitely worth exploring in a future post.
6 Just didn't have time to install Python at the time, but it's on my to-do list.
7 This is the sort of thing that makes me want to spend the rest of the day reading more on the internet instead of working. Thanks, Python.

Slán go fóill,
Shelby at 15:30