GitHub repo tree generator I have built a generator that, using the GitHub API, creates a dictionary containing a tree of all the resources in any GitHub repo. It uses the function git_tree, which takes one argument, a repo-string (repo-author/repo-name). For example, this repo only contains one file, a README. If we pass in the repo-string, and format the end result, this is what we get:

&gt;&gt; json.dumps(git_tree("githubtraining/github-move"), indent=3)
{
   "github-move": {
      "files": [
         "README.md"
      ],
      "dirs": {}
   }
}


You can see that it returns a dict with a key/value pair of {repo_name: tree}. So the github_move item contains a list of all files in that directory, and a dict with more nested directories. Obviously, in this repository there aren't any other directories, so that dict is just blank.

For sake of purpose, here is the tree of this repo (it was too long to put in the post). You can see each directory and subdirectory has its own files list and dirs dict.

Here's the code (repl.it online program for testing):

import requests
from pprint import pprint
from functools import reduce
import operator
import json
from itertools import chain, repeat, islice

class GitError(Exception): pass

def intersperse(delimiter, seq):
  return list(islice(chain.from_iterable(zip(repeat(delimiter), seq)), 1, None))

def _get_from_dict(dataDict, mapList):
  return reduce(operator.getitem, mapList, dataDict)

def _append_in_dict(dataDict, mapList, value):
  _get_from_dict(dataDict, mapList[:-1]).append(value)

def _get_sha(author, repo):
  try:
    return requests.get('https://api.github.com/repos/{}/{}/branches/master'.format(author, repo)).json()['commit']['commit']['tree']['sha']
  except KeyError as ex:
    raise GitError("Invalid author or repo name") from ex

def _get_git_tree(author, repo):
  return requests.get("https://api.github.com/repos/{}/{}/git/trees/{}?recursive=1".format(author, repo, _get_sha(author, repo))).json()["tree"]

def git_tree(repostring):
  author, repo = repostring.split("/")
  tree = {repo: {"files": [], "dirs": {}}}
  for token in _get_git_tree(author, repo):
    if token["type"] == "tree" and "/" not in token["path"]:
      tree[repo]["dirs"].update({token["path"]: {}})
      tree[repo]["dirs"][token["path"]].update({"files": [], "dirs": {}})
    elif token["type"] == "tree" and "/" in token["path"]:
      temp_dict = {}
      a = list(reversed(token["path"].split("/")))
      for k in a[:-1]:
        temp_dict = {k: {"files": [], "dirs": temp_dict}}
      tree[repo]["dirs"][a[-1]]["dirs"] = temp_dict
    elif token["type"] == "blob":
      path = token["path"].split("/")
      if len(path) == 1:
        tree[repo]["files"].append(path[0])
      else:
        dict_path = [repo, "dirs"] + intersperse("dirs", path[:-1]) + ["files", path[-1]]
        _append_in_dict(tree, dict_path, dict_path[-1])
  return tree

print(json.dumps(git_tree("githubtraining/caption-this"), indent=3))


(The json.dumps is just there for easy viewing, it can be ommited).

My questions:


Is it too messy? I look back at this function and it looks a bit cluttered and all over the place. Is that the case, or am I just going crazy?
Do I have any unnecessary code in there?
Is there anything else you deem wrong with the program?
I see nothing wrong with the code itself, but you're in dire need of docstrings. Arcane functional one-liners like the one in intersperse are impenetrable unless they're well-documented. You'd also benefit from splitting that one into multiple lines.

For strings like this:

    return requests.get('https://api.github.com/repos/{}/{}/branches/master'.format(author, repo)).json()['commit']['commit']['tree']['sha']


consider rewriting your format call as an f-string; i.e.

f'https://api.github.com/repos/{author}/{repo}/branches/master'