{"action":"created","comment":{"url":"https://api.github.com/repos/pyta-uoft/pyta/pulls/comments/73989552","id":73989552,"diff_hunk":"@@ -1,288 +1,94 @@\n-\"\"\"\n-BNF for csc108 docstring:\n-    set = \"set\" \"of\" element\n-    dict = \"dict\" \"of\" \"{\" element \",\" element \"}\"\n-    tuple = \"tuple\" \"of\" \"(\" elements \")\"\n-    list = \"list\" \"of\" element\n-    collection = list | tuple | dict | set\n-    element = collection | <ANY_STRING>\n-    elements = element (\",\" element)*\n-    typeContract = \"(\" elements \")\" \"->\" element\n-\"\"\"\n+import typing\n+from funcparserlib.parser import a, forward_decl, finished, many, maybe, skip, oneplus, some\n+from tokenize import generate_tokens\n+from io import StringIO\n \n-_VALID_TOKENS = [',', '(', ')', '[', ']', '{', '}']\n-_COLLECTION_TYPES = ['list', 'set', 'tuple', 'dict']\n \n+def a_str(s):\n+    return some(lambda x: x.string == s)\n \n-class NotA108DocstringException(Exception):\n-    \"\"\"Indicates a parsing exception (malformed docstring header).\"\"\"\n-    pass\n \n+def skip_str(s):\n+    return skip(some(lambda x: x.string == s))\n \n-class UnexpectedToken(Exception):\n-    \"\"\"Raised when an unexpected character is in the docstring.\"\"\"\n-    pass\n \n+def combine_elements_to_list(data):\n+    output_list = [x for x in data[0]]\n+    output_list.append(data[-1])\n+    return output_list\n \n-class DocNode:\n-    \"\"\"Represents a node in a tree of nodes which can be analyzed to get the\n-    parsed elements of a docstring header.\n-    \"\"\"\n-    def __init__(self, node_type=None):\n-        self.children = []\n-        self.node_type = node_type\n \n+def compile_list_type(data):\n+    return typing.List[data[1]]\n \n-def tokenize_docstring(docstr):\n-    \"\"\"Tokenizes a docstring into tokens for the parser.\n-    Accepts a docstring, and returns a list of tokens.\n-    \"\"\"\n-    tokens = []\n-    current_token = ''\n-    should_skip = False\n-    for i in range(len(docstr)):\n-        if should_skip:\n-            should_skip = False\n-            continue\n-        c = docstr[i]\n-        if c in _VALID_TOKENS:\n-            if len(current_token) > 0:\n-                tokens.append(current_token)\n-                current_token = ''\n-            tokens.append(c)\n-        elif c == '-':\n-            if i >= len(docstr) - 1:\n-                raise UnexpectedToken('Expected -> but got docstring end')\n-            next_c = docstr[i+1]\n-            if next_c != '>':\n-                raise UnexpectedToken('Expected -> but got -' + next_c)\n-            if len(current_token) > 0:\n-                tokens.append(current_token)\n-                current_token = ''\n-            tokens.append('->')\n-            should_skip = True  # TODO - need to advance iterator here by one extra...\n-        elif c.isalpha():\n-            current_token += c\n-        elif c == ' ' or c == '\\t':\n-            if len(current_token) > 0:\n-                tokens.append(current_token)\n-                current_token = ''\n-        else:\n-            raise UnexpectedToken('Unexpected docstring character: ' + c)\n-    # We will likely have a leftover unprocessed token at the end, handle it.\n-    if len(current_token) > 0:\n-        tokens.append(current_token)\n-    return tokens\n \n+def compile_set_type(data):\n+    return typing.Set[data[1]]\n \n-class DocstringParser:\n-    \"\"\"Used in parsing the recursive nature of a docstring.\n-    Upon calling the constructor, the object will begin processing the docstring\n-    line immediately, so upon returning there will either be an exception due to\n-    a malformed string, or the developed.\n-    Can throw either NotA108DocstringException or UnexpectedToken from the\n-    constructor.\"\"\"\n-    def __init__(self, entire_docstring):\n-        \"\"\"Creates an object that will have the entire parsed docstring set in\n-        the args_node and return_node DocNode objects.\n-        entire_docstring: The docstring to parse, which does not have to be a\n-        single line.\n-        Raises UnexpectedToken if the tokenizer detects an invalid token.\n-        Raises NotA108DocstringException if the docstring is not in a form\n-        that can be parsed properly (implies malformed docstring).\n-        \"\"\"\n-        docstring_header = trim_docstring_down_to_type_contract(entire_docstring)\n-        self.args_node = DocNode()\n-        self.return_node = DocNode()\n-        self.node_stack = [self.args_node]\n-        self.tokens = tokenize_docstring(docstring_header)\n-        self.token_index = 0\n-        try:\n-            self.parse_type_contract()\n-        except IndexError:\n-            raise NotA108DocstringException\n \n-    def parse_type_contract(self):\n-        \"\"\"Parses the entire type contract, of which it will have raised an\n-        exception or fully parsed the docstring.\n-        \"\"\"\n-        self.consume_token_type_or_error('(')\n-        self.parse_elements_until_right_paren()\n-        self.consume_token_type_or_error(')')\n-        self.consume_token_type_or_error('->')\n-        self.parse_elements_until_end()\n-        \n-    def parse_elements_until_right_paren(self):\n-        \"\"\"Parses the docstring data to the left of the arrow and inside the\n-        brackets. Raises an exception if it is not a proper docstring.\n-        \"\"\"\n-        if self.get_current_token() == ')':\n-            return\n-        self.parse_element()\n-        while self.get_current_token() == ',':\n-            self.consume_token_type_or_error(',')\n-            self.parse_element()\n+def compile_dict_type(data):\n+    return typing.Dict[data[1], data[2]]\n \n-    def parse_elements_until_end(self):\n-        \"\"\"Parses the results side of the docstring. Raises an exception if it\n-        is not a proper docstring.\n-        \"\"\"\n-        self.node_stack = [self.return_node]\n-        self.parse_element()\n-        self.raise_if_not_all_tokens_consumed()\n-        \n-    def parse_element(self):\n-        \"\"\"Parses an element recursively in a docstring. Raises an exception\n-        if it is not a proper docstring.\n-        \"\"\"\n-        node_type = self.get_current_token()\n-        self.advance_to_next_token()\n-        if node_type in _COLLECTION_TYPES:\n-            self.consume_token_type_or_error('of')\n-            if node_type in ['list', 'set']:\n-                self.parse_list_or_set(node_type)\n-            elif node_type == 'dict':\n-                self.parse_dict()\n-            elif node_type == 'tuple':\n-                self.parse_tuple()\n-            else:\n-                raise NotA108DocstringException('Unexpected collection: ' + node_type)\n-        else:\n-            self.add_as_child_node(DocNode(node_type))\n \n-    def parse_list_or_set(self, node_type):\n-        \"\"\"Parses a list or set. Raises an exception if it is not a proper\n-        docstring.\n-        node_type: The node type to be set (should be 'list' or 'set').\n-        \"\"\"\n-        node = DocNode(node_type)\n-        self.add_as_child_node(node)\n-        self.push_node_on_stack(node)\n-        self.parse_element()\n-        self.pop_node_stack()\n+# Despite the data being a list of class types, Tuple[...] will not accept a\n+# list, but will accept a tuple() that was converted from a list.\n+def compile_tuple_type(data):\n+    return typing.Tuple[tuple(data[-1])]\n \n-    def parse_dict(self):\n-        \"\"\"Parses a dictionary element recursively. Raises an exception if it\n-        is not a proper docstring.\n-        \"\"\"\n-        self.consume_token_type_or_error('{')\n-        node = DocNode('dict')\n-        self.add_as_child_node(node)\n-        self.push_then_parse_then_pop(node)\n-        self.consume_token_type_or_error(',')\n-        self.push_then_parse_then_pop(node)\n-        self.consume_token_type_or_error('}')\n \n-    def parse_tuple(self):\n-        \"\"\"Parses a tuple recursively. Raises an exception if it is not a\n-        proper docstring.\n-        \"\"\"\n-        self.consume_token_type_or_error('(')\n-        node = DocNode('tuple')\n-        self.add_as_child_node(node)\n-        self.push_then_parse_then_pop(node)\n-        while self.get_current_token() == ',':\n-            self.consume_token_type_or_error(',')\n-            self.push_then_parse_then_pop(node)\n-        self.consume_token_type_or_error(')')\n+def to_simple_type(token):\n+    s = token.string\n+    if s == 'int':\n+        return int\n+    elif s == 'str':\n+        return str\n+    elif s == 'float':\n+        return float\n+    elif s == 'bool':\n+        return bool\n+    elif s in ['obj', 'object']:\n+        return typing.Any\n+    elif s in ['None', 'NoneType']:\n+        return type(None)\n+    return s\n \n-    def consume_token_type_or_error(self, token_str):\n-        \"\"\"Consumes the token that is provided in the argument. If the token\n-        is not found, then it will throw a NotA108DocstringException. The\n-        stream will be advanced.\n-        token_str: The string that should be found on the current token in the\n-        stream.\n-        \"\"\"\n-        if self.token_index >= len(self.tokens):\n-            raise NotA108DocstringException('Expected \"%s\", terminated early' % token_str)\n-        token = self.get_current_token()\n-        if token != token_str:\n-            raise NotA108DocstringException('Expected \"%s\", got \"%s\" instead' % (token_str, token))\n-        self.advance_to_next_token()\n \n-    def push_then_parse_then_pop(self, node):\n-        \"\"\"A convenience method that will push the node onto the stack, then\n-        parse the element recursively, then pop this node off. This is used to\n-        push a node that has child definitions onto the stack so it is\n-        assigned the children appropriately, and then cleans up the stack.\n-        node: The DocNode to go on the stack.\n-        \"\"\"\n-        self.push_node_on_stack(node)\n-        self.parse_element()\n-        self.pop_node_stack()\n+# -----------------------------------------------------------------------------\n \n-    def get_current_token(self):\n-        \"\"\"Gets the current token.\"\"\"\n-        return self.tokens[self.token_index]\n+element = forward_decl()\n+elements = forward_decl()\n \n-    def advance_to_next_token(self):\n-        \"\"\"Advances to the next token. Note: This does not throw an exception\n-        if there is not a following token, the pointer is incremented.\"\"\"\n-        self.token_index += 1\n+skip_of = skip_str('of')\n+any_class = some(lambda x: True) >> to_simple_type\n \n-    def add_as_child_node(self, node):\n-        \"\"\"Adds a node as a child to the last element on the node stack.\n-        node: The node to add.\n-        \"\"\"\n-        self.node_stack[-1].children.append(node)\n+set_parser = (a_str('set') + skip_of + element) >> compile_set_type\n \n-    def push_node_on_stack(self, node):\n-        \"\"\"Pushes a node onto the stack.\n-        node: The DocNode to push onto the stack.\n-        \"\"\"\n-        self.node_stack.append(node)\n+list_parser = (a_str('list') + skip_of + element) >> compile_list_type\n \n-    def pop_node_stack(self):\n-        \"\"\"Pops a node off of the stack.\"\"\"\n-        self.node_stack.pop(-1)\n+dict_parser = (a_str('dict') + skip_of + skip_str('{') + element + skip_str(',') + element + skip_str('}')) >> \\\n+              compile_dict_type\n \n-    def raise_if_not_all_tokens_consumed(self):\n-        \"\"\"Checks to make sure all the tokens have been consumed, which is\n-        required for the final parsing step since the user may have added\n-        additional tokens that make it an invalid string.\n-        Example: '(int) -> str bool' is invalid.\n-        \"\"\"\n-        if self.token_index < len(self.tokens):\n-            raise NotA108DocstringException\n+tuple_parser = (a_str('tuple') + skip_of + skip_str('(') + (elements >> combine_elements_to_list) + skip_str(')')) >> \\\n+               compile_tuple_type\n \n-    \n-def trim_docstring_down_to_type_contract(full_docstring):\n-    \"\"\"Extracts the line we want (the type contract) from the entire docstring.\n-    This assumes there always will be a docstring, whereby it will read the top\n-    line and continue reading until it hits an empty line, of which it will then\n-    assume the docstring header to be done. This allows for a docstring to be\n-    multiple lines, like:\n-        (int, set of list of tuple of(int, str), obj)\n-            ->\n-                tuple of (int, int)\n-    This is needed since PEP-8 being enforced in CSC108 means the possibility of\n-    the docstring being greater than 80 characters, and it must be supported if\n-    its too long.\n-    This function will properly extract the string and compress it into a single\n-    string with all newlines/improper spacing removed.\n-    Note: This does not check for correctness of the docstring header contract,\n-    its job is only to extract the docstring, and let the parser error out if\n-    the type contract is malformed.\n-    \"\"\"\n-    lines = full_docstring.split('\\n')  # TODO - Support \\r\\n too.\n-    merged_docstring = ''\n-    for line in lines:\n-        stripped_line = line.strip()\n-        if len(stripped_line) <= 0:\n-            break\n-        # Lack of whitespace between lines could cause problems, so we will\n-        # assume there is supposed to be a space to replace the new line.\n-        # Since the parser will skip any extra space, this is safe to do.\n-        merged_docstring += stripped_line + ' '\n-    return merged_docstring[:-1]  # Trim last space (TODO - do this better...)\n+element.define(set_parser | list_parser | dict_parser | tuple_parser | any_class)\n+elements.define(maybe(many(element + skip_str(','))) + element)\n \n+# TODO - Need to strip a [] leftover if there's one element at the beginning.\n+docstring_parser = skip_str('(') + elements + skip_str(')') + skip_str('->') + element\n \n-def parse_csc108_docstring(full_docstring):\n-    \"\"\"Takes the header line of a docstring which is supposed to be the\n-    docstring, and parses it. If parsing fails, a NotA108DocstringException\n-    is raised.\n-    Returns two nodes as a tuple: (Args tree DocNode, result tree DocNode).\n-    \"\"\"\n-    line = trim_docstring_down_to_type_contract(full_docstring)\n-    parser = DocstringParser(line)\n-    return parser.args_node, parser.return_node\n+# -----------------------------------------------------------------------------\n+\n+# non_right_arrow = some(lambda x: x != '>')\n+# single_arrow = a_str('>') + non_right_arrow\n+# double_arrow = a_str('>>') + non_right_arrow\n+# docstring_description = oneplus(non_right_arrow | single_arrow | double_arrow) >> to_str\n+\n+# docstring_doctest = a_str('>>>') + (oneplus(some(is_valid_char)) >> to_str)\n+\n+# -----------------------------------------------------------------------------\n+\n+docstring_raw = '(dict of {SomeFakeClass, tuple of (bool, int, str, NoneType)}, int) -> list of list of SomeClass'","path":"docstring/csc108_docstring.py","position":352,"original_position":352,"commit_id":"de02ead62e30a8a038ee00cba1e4b184239d87ac","original_commit_id":"de02ead62e30a8a038ee00cba1e4b184239d87ac","user":{"login":"david-yz-liu","id":3333115,"avatar_url":"https://avatars.githubusercontent.com/u/3333115?v=3","gravatar_id":"","url":"https://api.github.com/users/david-yz-liu","html_url":"https://github.com/david-yz-liu","followers_url":"https://api.github.com/users/david-yz-liu/followers","following_url":"https://api.github.com/users/david-yz-liu/following{/other_user}","gists_url":"https://api.github.com/users/david-yz-liu/gists{/gist_id}","starred_url":"https://api.github.com/users/david-yz-liu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/david-yz-liu/subscriptions","organizations_url":"https://api.github.com/users/david-yz-liu/orgs","repos_url":"https://api.github.com/users/david-yz-liu/repos","events_url":"https://api.github.com/users/david-yz-liu/events{/privacy}","received_events_url":"https://api.github.com/users/david-yz-liu/received_events","type":"User","site_admin":false},"body":"Please remove this code (and the commented-out block above it). If you want, you can move the string into the test file.","created_at":"2016-08-09T02:27:03Z","updated_at":"2016-08-09T02:27:03Z","html_url":"https://github.com/pyta-uoft/pyta/pull/92#discussion_r73989552","pull_request_url":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92","_links":{"self":{"href":"https://api.github.com/repos/pyta-uoft/pyta/pulls/comments/73989552"},"html":{"href":"https://github.com/pyta-uoft/pyta/pull/92#discussion_r73989552"},"pull_request":{"href":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92"}}},"pull_request":{"url":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92","id":79515960,"html_url":"https://github.com/pyta-uoft/pyta/pull/92","diff_url":"https://github.com/pyta-uoft/pyta/pull/92.diff","patch_url":"https://github.com/pyta-uoft/pyta/pull/92.patch","issue_url":"https://api.github.com/repos/pyta-uoft/pyta/issues/92","number":92,"state":"open","locked":false,"title":"Docstring108 sandbox","user":{"login":"WChrisK","id":3758824,"avatar_url":"https://avatars.githubusercontent.com/u/3758824?v=3","gravatar_id":"","url":"https://api.github.com/users/WChrisK","html_url":"https://github.com/WChrisK","followers_url":"https://api.github.com/users/WChrisK/followers","following_url":"https://api.github.com/users/WChrisK/following{/other_user}","gists_url":"https://api.github.com/users/WChrisK/gists{/gist_id}","starred_url":"https://api.github.com/users/WChrisK/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/WChrisK/subscriptions","organizations_url":"https://api.github.com/users/WChrisK/orgs","repos_url":"https://api.github.com/users/WChrisK/repos","events_url":"https://api.github.com/users/WChrisK/events{/privacy}","received_events_url":"https://api.github.com/users/WChrisK/received_events","type":"User","site_admin":false},"body":"This is mainly for looking at, there's 2 tests that fail and I'm still trying to figure out why, hopefully getting some sleep will possibly help me solve this tomorrow.","created_at":"2016-08-01T05:43:15Z","updated_at":"2016-08-09T02:27:03Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7d69a0ba315e38992156b6e101df8310ce5a93ed","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92/commits","review_comments_url":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92/comments","review_comment_url":"https://api.github.com/repos/pyta-uoft/pyta/pulls/comments{/number}","comments_url":"https://api.github.com/repos/pyta-uoft/pyta/issues/92/comments","statuses_url":"https://api.github.com/repos/pyta-uoft/pyta/statuses/de02ead62e30a8a038ee00cba1e4b184239d87ac","head":{"label":"WChrisK:docstring108_sandbox","ref":"docstring108_sandbox","sha":"de02ead62e30a8a038ee00cba1e4b184239d87ac","user":{"login":"WChrisK","id":3758824,"avatar_url":"https://avatars.githubusercontent.com/u/3758824?v=3","gravatar_id":"","url":"https://api.github.com/users/WChrisK","html_url":"https://github.com/WChrisK","followers_url":"https://api.github.com/users/WChrisK/followers","following_url":"https://api.github.com/users/WChrisK/following{/other_user}","gists_url":"https://api.github.com/users/WChrisK/gists{/gist_id}","starred_url":"https://api.github.com/users/WChrisK/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/WChrisK/subscriptions","organizations_url":"https://api.github.com/users/WChrisK/orgs","repos_url":"https://api.github.com/users/WChrisK/repos","events_url":"https://api.github.com/users/WChrisK/events{/privacy}","received_events_url":"https://api.github.com/users/WChrisK/received_events","type":"User","site_admin":false},"repo":{"id":57931558,"name":"pyta","full_name":"WChrisK/pyta","owner":{"login":"WChrisK","id":3758824,"avatar_url":"https://avatars.githubusercontent.com/u/3758824?v=3","gravatar_id":"","url":"https://api.github.com/users/WChrisK","html_url":"https://github.com/WChrisK","followers_url":"https://api.github.com/users/WChrisK/followers","following_url":"https://api.github.com/users/WChrisK/following{/other_user}","gists_url":"https://api.github.com/users/WChrisK/gists{/gist_id}","starred_url":"https://api.github.com/users/WChrisK/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/WChrisK/subscriptions","organizations_url":"https://api.github.com/users/WChrisK/orgs","repos_url":"https://api.github.com/users/WChrisK/repos","events_url":"https://api.github.com/users/WChrisK/events{/privacy}","received_events_url":"https://api.github.com/users/WChrisK/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/WChrisK/pyta","description":"Adventures in code analysis for teaching Python.","fork":true,"url":"https://api.github.com/repos/WChrisK/pyta","forks_url":"https://api.github.com/repos/WChrisK/pyta/forks","keys_url":"https://api.github.com/repos/WChrisK/pyta/keys{/key_id}","collaborators_url":"https://api.github.com/repos/WChrisK/pyta/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/WChrisK/pyta/teams","hooks_url":"https://api.github.com/repos/WChrisK/pyta/hooks","issue_events_url":"https://api.github.com/repos/WChrisK/pyta/issues/events{/number}","events_url":"https://api.github.com/repos/WChrisK/pyta/events","assignees_url":"https://api.github.com/repos/WChrisK/pyta/assignees{/user}","branches_url":"https://api.github.com/repos/WChrisK/pyta/branches{/branch}","tags_url":"https://api.github.com/repos/WChrisK/pyta/tags","blobs_url":"https://api.github.com/repos/WChrisK/pyta/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/WChrisK/pyta/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/WChrisK/pyta/git/refs{/sha}","trees_url":"https://api.github.com/repos/WChrisK/pyta/git/trees{/sha}","statuses_url":"https://api.github.com/repos/WChrisK/pyta/statuses/{sha}","languages_url":"https://api.github.com/repos/WChrisK/pyta/languages","stargazers_url":"https://api.github.com/repos/WChrisK/pyta/stargazers","contributors_url":"https://api.github.com/repos/WChrisK/pyta/contributors","subscribers_url":"https://api.github.com/repos/WChrisK/pyta/subscribers","subscription_url":"https://api.github.com/repos/WChrisK/pyta/subscription","commits_url":"https://api.github.com/repos/WChrisK/pyta/commits{/sha}","git_commits_url":"https://api.github.com/repos/WChrisK/pyta/git/commits{/sha}","comments_url":"https://api.github.com/repos/WChrisK/pyta/comments{/number}","issue_comment_url":"https://api.github.com/repos/WChrisK/pyta/issues/comments{/number}","contents_url":"https://api.github.com/repos/WChrisK/pyta/contents/{+path}","compare_url":"https://api.github.com/repos/WChrisK/pyta/compare/{base}...{head}","merges_url":"https://api.github.com/repos/WChrisK/pyta/merges","archive_url":"https://api.github.com/repos/WChrisK/pyta/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/WChrisK/pyta/downloads","issues_url":"https://api.github.com/repos/WChrisK/pyta/issues{/number}","pulls_url":"https://api.github.com/repos/WChrisK/pyta/pulls{/number}","milestones_url":"https://api.github.com/repos/WChrisK/pyta/milestones{/number}","notifications_url":"https://api.github.com/repos/WChrisK/pyta/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/WChrisK/pyta/labels{/name}","releases_url":"https://api.github.com/repos/WChrisK/pyta/releases{/id}","deployments_url":"https://api.github.com/repos/WChrisK/pyta/deployments","created_at":"2016-05-03T01:09:51Z","updated_at":"2016-05-03T01:09:52Z","pushed_at":"2016-08-08T03:51:24Z","git_url":"git://github.com/WChrisK/pyta.git","ssh_url":"git@github.com:WChrisK/pyta.git","clone_url":"https://github.com/WChrisK/pyta.git","svn_url":"https://github.com/WChrisK/pyta","homepage":null,"size":215,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"pyta-uoft:master","ref":"master","sha":"992e2564515fcc9136e35c39b3dbab954a9b7760","user":{"login":"pyta-uoft","id":18420574,"avatar_url":"https://avatars.githubusercontent.com/u/18420574?v=3","gravatar_id":"","url":"https://api.github.com/users/pyta-uoft","html_url":"https://github.com/pyta-uoft","followers_url":"https://api.github.com/users/pyta-uoft/followers","following_url":"https://api.github.com/users/pyta-uoft/following{/other_user}","gists_url":"https://api.github.com/users/pyta-uoft/gists{/gist_id}","starred_url":"https://api.github.com/users/pyta-uoft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pyta-uoft/subscriptions","organizations_url":"https://api.github.com/users/pyta-uoft/orgs","repos_url":"https://api.github.com/users/pyta-uoft/repos","events_url":"https://api.github.com/users/pyta-uoft/events{/privacy}","received_events_url":"https://api.github.com/users/pyta-uoft/received_events","type":"Organization","site_admin":false},"repo":{"id":52238733,"name":"pyta","full_name":"pyta-uoft/pyta","owner":{"login":"pyta-uoft","id":18420574,"avatar_url":"https://avatars.githubusercontent.com/u/18420574?v=3","gravatar_id":"","url":"https://api.github.com/users/pyta-uoft","html_url":"https://github.com/pyta-uoft","followers_url":"https://api.github.com/users/pyta-uoft/followers","following_url":"https://api.github.com/users/pyta-uoft/following{/other_user}","gists_url":"https://api.github.com/users/pyta-uoft/gists{/gist_id}","starred_url":"https://api.github.com/users/pyta-uoft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pyta-uoft/subscriptions","organizations_url":"https://api.github.com/users/pyta-uoft/orgs","repos_url":"https://api.github.com/users/pyta-uoft/repos","events_url":"https://api.github.com/users/pyta-uoft/events{/privacy}","received_events_url":"https://api.github.com/users/pyta-uoft/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/pyta-uoft/pyta","description":"Adventures in code analysis for teaching Python.","fork":false,"url":"https://api.github.com/repos/pyta-uoft/pyta","forks_url":"https://api.github.com/repos/pyta-uoft/pyta/forks","keys_url":"https://api.github.com/repos/pyta-uoft/pyta/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pyta-uoft/pyta/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pyta-uoft/pyta/teams","hooks_url":"https://api.github.com/repos/pyta-uoft/pyta/hooks","issue_events_url":"https://api.github.com/repos/pyta-uoft/pyta/issues/events{/number}","events_url":"https://api.github.com/repos/pyta-uoft/pyta/events","assignees_url":"https://api.github.com/repos/pyta-uoft/pyta/assignees{/user}","branches_url":"https://api.github.com/repos/pyta-uoft/pyta/branches{/branch}","tags_url":"https://api.github.com/repos/pyta-uoft/pyta/tags","blobs_url":"https://api.github.com/repos/pyta-uoft/pyta/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pyta-uoft/pyta/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pyta-uoft/pyta/git/refs{/sha}","trees_url":"https://api.github.com/repos/pyta-uoft/pyta/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pyta-uoft/pyta/statuses/{sha}","languages_url":"https://api.github.com/repos/pyta-uoft/pyta/languages","stargazers_url":"https://api.github.com/repos/pyta-uoft/pyta/stargazers","contributors_url":"https://api.github.com/repos/pyta-uoft/pyta/contributors","subscribers_url":"https://api.github.com/repos/pyta-uoft/pyta/subscribers","subscription_url":"https://api.github.com/repos/pyta-uoft/pyta/subscription","commits_url":"https://api.github.com/repos/pyta-uoft/pyta/commits{/sha}","git_commits_url":"https://api.github.com/repos/pyta-uoft/pyta/git/commits{/sha}","comments_url":"https://api.github.com/repos/pyta-uoft/pyta/comments{/number}","issue_comment_url":"https://api.github.com/repos/pyta-uoft/pyta/issues/comments{/number}","contents_url":"https://api.github.com/repos/pyta-uoft/pyta/contents/{+path}","compare_url":"https://api.github.com/repos/pyta-uoft/pyta/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pyta-uoft/pyta/merges","archive_url":"https://api.github.com/repos/pyta-uoft/pyta/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pyta-uoft/pyta/downloads","issues_url":"https://api.github.com/repos/pyta-uoft/pyta/issues{/number}","pulls_url":"https://api.github.com/repos/pyta-uoft/pyta/pulls{/number}","milestones_url":"https://api.github.com/repos/pyta-uoft/pyta/milestones{/number}","notifications_url":"https://api.github.com/repos/pyta-uoft/pyta/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pyta-uoft/pyta/labels{/name}","releases_url":"https://api.github.com/repos/pyta-uoft/pyta/releases{/id}","deployments_url":"https://api.github.com/repos/pyta-uoft/pyta/deployments","created_at":"2016-02-22T01:38:12Z","updated_at":"2016-06-28T21:59:16Z","pushed_at":"2016-08-09T02:10:40Z","git_url":"git://github.com/pyta-uoft/pyta.git","ssh_url":"git@github.com:pyta-uoft/pyta.git","clone_url":"https://github.com/pyta-uoft/pyta.git","svn_url":"https://github.com/pyta-uoft/pyta","homepage":null,"size":288,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":11,"forks":6,"open_issues":11,"watchers":8,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92"},"html":{"href":"https://github.com/pyta-uoft/pyta/pull/92"},"issue":{"href":"https://api.github.com/repos/pyta-uoft/pyta/issues/92"},"comments":{"href":"https://api.github.com/repos/pyta-uoft/pyta/issues/92/comments"},"review_comments":{"href":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92/comments"},"review_comment":{"href":"https://api.github.com/repos/pyta-uoft/pyta/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/pyta-uoft/pyta/pulls/92/commits"},"statuses":{"href":"https://api.github.com/repos/pyta-uoft/pyta/statuses/de02ead62e30a8a038ee00cba1e4b184239d87ac"}}}}
{"actor":{"display_login":"david-yz-liu"}}