Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Representer Updates and Golden Tests #45

Merged
merged 20 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a82d8fe
Add practice exericse golden files.
BethanyG Dec 25, 2023
6b12896
Add concept exercise golden files.
BethanyG Dec 25, 2023
c921071
Add example test golden files.
BethanyG Dec 25, 2023
da38349
Created run in docker script to build docker container and run golden…
BethanyG Dec 25, 2023
a12fb2e
Added CI workflow to test with golden files.
BethanyG Dec 25, 2023
ab89953
Changed Dockerfile to use Alpine and updated run scripts and requirem…
BethanyG Dec 25, 2023
6daf976
Removed astor lib and replaced it with Python 3.11 AST Lib
BethanyG Dec 25, 2023
2d6804e
Added metadata property, and adjusted script to output metadata.json.
BethanyG Dec 25, 2023
792c4d1
Added golden tests for concept, practice, and example exercises.
BethanyG Dec 25, 2023
75aaca4
Small adjustments.
BethanyG Dec 25, 2023
af4b3d3
Removed unneeded example.py files.
BethanyG Dec 25, 2023
cbc2534
Merge branch 'main' into fix-representations
BethanyG Dec 25, 2023
09934f1
Re-ran golden files for practice exercises.
BethanyG Jan 3, 2024
6b836f1
Re-ran golden files for concept exercises.
BethanyG Jan 3, 2024
ddca18a
Re-ran golden files for example tests.
BethanyG Jan 3, 2024
4f5e021
Pinned Ubuntu version to 22.04
BethanyG Jan 3, 2024
4909027
Removed curl, bash, and apk updates from dockerfile.
BethanyG Jan 3, 2024
68a61d4
Added cache clean, but thought better of it.
BethanyG Jan 3, 2024
ed90eb7
Moved normalized code to first position in representer.out file.
BethanyG Jan 3, 2024
908a5c6
changed job name to test from build.
BethanyG Mar 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
.gitattributes
.dockerignore
Dockerfile
test/
bin/
!bin/run.py
!bin/run.sh
38 changes: 25 additions & 13 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,36 @@ on:
branches:
- main
paths-ignore:
- '.gitignore'
- 'LICENSE'
- '**.md'
- ".gitignore"
- "LICENSE"
- "**.md"
push:
paths-ignore:
- '.gitignore'
- 'LICENSE'
- '**.md'

- ".gitignore"
- "LICENSE"
- "**.md"
jobs:
test:
name: Test Representer
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e
- name: Checkout code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4c0219f9ac95b02789c1075625400b2acbff50b1
with:
install: true

- name: Build Docker image and store in cache
uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825
with:
context: .
push: false
load: true
tags: exercism/python-representer
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Build Docker Image
run: docker build -f Dockerfile -t python-representer .

- name: Run Tests
run: docker run --entrypoint "pytest" python-representer
- name: Run Tests in Docker
run: bin/run-tests-in-docker.sh
21 changes: 2 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
FROM python:3.11.2-slim as base

FROM base as builder

RUN mkdir /install

WORKDIR /install
FROM python:3.11.5-alpine3.18

COPY requirements.txt /requirements.txt
COPY dev-requirements.txt /dev-requirements.txt

RUN pip install --prefix=/install --no-warn-script-location -r /requirements.txt -r /dev-requirements.txt

FROM base

COPY --from=builder /install /usr/local


RUN apt-get update \
&& apt-get install curl -y \
&& apt-get remove curl -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
RUN pip install -r /requirements.txt -r /dev-requirements.txt

COPY . /opt/representer

Expand Down
9 changes: 7 additions & 2 deletions bin/run-in-docker.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
set -e

# Synopsis:
Expand Down Expand Up @@ -32,8 +32,13 @@ docker build --rm --no-cache -t python-representer .
output_dir="$3"
mkdir -p "$output_dir"

# run image passing the arguments
# run image passing the arguments
docker run \
--rm \
--network none \
--read-only \
--mount type=bind,src=$PWD/$2,dst=/solution \
--mount type=bind,src=$PWD/$output_dir,dst=/output \
python-representer $1 /solution/ /output/
--mount type=tmpfs,destination=/tmp \
exercism/python-representer $1 /solution/ /output/
30 changes: 30 additions & 0 deletions bin/run-tests-in-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env sh

# Synopsis:
# Test the test runner Docker image by running it against a predefined set of
# solutions with an expected output.
# The test runner Docker image is built automatically.

# Output:
# Outputs the diff of the expected test results against the actual test results
# generated by the test runner Docker image.

# Example:
# ./bin/run-tests-in-docker.sh

# Stop executing when a command returns a non-zero return code
set -e

# Build the Docker image
docker build --rm -t exercism/python-representer .

# Run the Docker image using the settings mimicking the production environment
docker run \
--rm \
--network none \
--read-only \
--mount type=bind,src="${PWD}/test",dst=/opt/representer/test \
--mount type=tmpfs,dst=/tmp \
--workdir /opt/representer \
--entrypoint pytest \
exercism/python-representer -v --disable-warnings
5 changes: 3 additions & 2 deletions bin/run.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#! /bin/sh
#! /usr/bin/env sh

root="$( dirname "$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )" )"
export PYTHONPATH="$root:$PYTHONPATH"
python bin/run.py "$@"
/usr/bin/env python3 bin/run.py "$@"
25 changes: 22 additions & 3 deletions representer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Representer for Python.
"""

import json
from typing import Dict

Expand All @@ -17,6 +18,7 @@ class Representer:
def __init__(self, source: str) -> None:
self._tree = utils.parse(source)
self._normalizer = Normalizer()
self.metadata = {"version" : 2}

def normalize(self) -> None:
"""
Expand All @@ -30,6 +32,12 @@ def dump_tree(self) -> str:
"""
return utils.dump_tree(self._tree)

def dump_ast(self) -> str:
"""
Dump the current stat of the tree without indents.
"""
return utils.dump_ast(self._tree)

def dump_code(self, reformat=True) -> str:
"""
Dump the current tree as generate code.
Expand All @@ -52,6 +60,12 @@ def dump_map(self) -> str:
"""
return utils.to_json(self.mapping)

def dump_metadata(self) -> Dict[str, int]:
"""
Dump the representer metadata.
"""
return utils.to_json(self.metadata)


def represent(slug: utils.Slug, input: utils.Directory, output: utils.Directory) -> None:
"""
Expand Down Expand Up @@ -80,21 +94,26 @@ def represent(slug: utils.Slug, input: utils.Directory, output: utils.Directory)
out_dst = output.joinpath("representation.out")
txt_dst = output.joinpath("representation.txt")
map_dst = output.joinpath("mapping.json")
metadata_dst = output.joinpath("representation.json")


# parse the tree from the file contents
representation = Representer(src.read_text())

# save dump of the initial tree for debug
out = ["# BEGIN TREE BEFORE", representation.dump_tree(), ""]
out = ['## BEGIN TREE BEFORE ##', representation.dump_tree(), '## END TREE BEFORE ##', '']

# normalize the tree
representation.normalize()

# save dump of normalized code for debug (from un-parsing the normalized AST).
out[0:0] = ['## BEGIN NORMALIZED CODE ##', representation.dump_code(), "## END NORMALIZED CODE ##", '']

# save dump of the normalized tree for debug
out.extend(["# BEGIN TREE AFTER", representation.dump_tree()])
out.extend(['## BEGIN NORMALIZED TREE ##', representation.dump_tree(), '## END NORMALIZED TREE ##'])

# dump the representation files
out_dst.write_text("\n".join(out))
txt_dst.write_text(representation.dump_code())
txt_dst.write_text(representation.dump_ast())
map_dst.write_text(representation.dump_map())
metadata_dst.write_text(representation.dump_metadata())
66 changes: 55 additions & 11 deletions representer/normalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,28 @@
Assign,
AsyncFunctionDef,
Attribute,
Call,
ClassDef,
Compare,
Constant,
DictComp,
Eq,
ExceptHandler,
Expr,
FunctionDef,
GeneratorExp,
Global,
If,
ListComp,
Load,
Module,
Name,
NodeTransformer,
Nonlocal,
SetComp,
Str,
Store,
Yield,
alias,
arg,
get_docstring,
Expand Down Expand Up @@ -140,6 +146,7 @@ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> AsyncFunctionDef:
def visit_arg(self, node: arg) -> arg:
"""
Arguments in definition signatures.
Drops type annotations.
"""
node.arg = self.add_placeholder(node.arg)
if node.annotation:
Expand Down Expand Up @@ -190,17 +197,6 @@ def visit_Nonlocal(self, node: Nonlocal) -> Nonlocal:
self.generic_visit(node)
return node

def visit_Expr(self, node: Expr) -> Optional[Expr]:
"""
Expressions not assigned to an identifier.
"""
if isinstance(node.value, Constant):
# eliminate registered docstrings
if utils.md5sum(node.value.value) in self._docstring_cache:
return None
self.generic_visit(node)
return node

@overload
def _visit_identifier(self, node: Name) -> Name:
...
Expand Down Expand Up @@ -231,6 +227,7 @@ def visit_Attribute(self, node: Attribute) -> Attribute:
"""
return self._visit_identifier(node)


@overload
def _visit_comprehension(self, node: ListComp) -> ListComp:
...
Expand Down Expand Up @@ -280,5 +277,52 @@ def visit_DictComp(self, node: DictComp) -> DictComp:
"""
return self._visit_comprehension(node)

@overload
def _visit_If(self, node: If) -> If:
...

def visit_If(self, node: If) -> None:
"""Remove if __name__ == '__main__' nodes.

Looks for ast.If that includes __name__ == __main__ checks
and removes the block.
"""
if isinstance(node.test, Compare):
if not (isinstance(node.test.left, Name) and node.test.left.id == '__name__'):
self.generic_visit(node)
return node
else:
if node.test.comparators[0].value == '__main__':
return None

self.generic_visit(node)
return node

def visit_Expr(self, node: Expr) -> Optional[Expr]:
"""Expressions not assigned to an identifier.

Removes print statements and docstrings from
the representation.
"""

# Remove print() statements from representation.
if (isinstance(node.value, Call) and
isinstance(node.value.func, Name)):
if node.value.func.id == 'print':
return None

# Pass through generator code.
if isinstance(node.value, Yield):
return node


# Eliminate previously registered docstrings
if not isinstance(node.value, Call):
if utils.md5sum(node.value.value) in self._docstring_cache:
return None

self.generic_visit(node)
return node


__all__ = ["Normalizer"]
21 changes: 15 additions & 6 deletions representer/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Representer for Python.
Representer Utilities for Python track.
"""


import ast
import errno
import json
Expand All @@ -11,12 +13,10 @@
from pathlib import Path
from typing import NewType

import astor
import black


Slug = NewType("Slug", str)

SLUG_RE = re.compile(r"^[a-z]+(-[a-z]+)*$")


Expand All @@ -41,6 +41,7 @@ def directory(string: str) -> Directory:
err = errno.ENOENT
msg = os.strerror(err)
raise FileNotFoundError(err, f"{msg}: {string!r}")

if not os.access(path, os.R_OK | os.W_OK):
err = errno.EACCES
msg = os.strerror(err)
Expand Down Expand Up @@ -80,19 +81,27 @@ def to_json(data: dict) -> str:
def parse(source: str) -> ast.AST:
"""
Wrapper around ast.parse.
Preserves type annotations.
"""
return ast.parse(source)
return ast.parse(source, type_comments=False, feature_version=(3,11))


def dump_tree(tree: ast.AST) -> str:
"""
Dump a formatted string of the AST.
"""
return astor.dump_tree(tree, indentation=" ", maxline=88)
return ast.dump(tree, annotate_fields=False, include_attributes=True, indent=2)


def dump_ast(tree: ast.AST) -> str:
"""
dump an un-formatted string of the AST.
"""
return ast.dump(tree, annotate_fields=False, include_attributes=True)


def to_source(tree: ast.AST) -> str:
"""
Dump the AST to generated source doe.
"""
return astor.to_source(tree)
return ast.unparse(tree)
Loading
Loading