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

feat: ast-based helpers #111

Merged
merged 35 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f7aff4c
feat: add ast helpers + build script
ojeytonwilliams Jan 11, 2024
9592fce
build: add python tests
ojeytonwilliams Jan 11, 2024
ecefda9
feat: use chainable class for helpers
ojeytonwilliams Jan 12, 2024
142868d
feat: add find_class
ojeytonwilliams Jan 12, 2024
f7eb116
chore: organise tests into suites
ojeytonwilliams Jan 12, 2024
a22f3b9
feat: add get and find for variables
ojeytonwilliams Jan 12, 2024
796b363
feat: add value_is_call helper
ojeytonwilliams Jan 12, 2024
4c3ddf8
feat: len, find_ifs and find_nth
ojeytonwilliams Jan 15, 2024
cac8d78
feat: make Chainable subscriptable
ojeytonwilliams Jan 15, 2024
30c3aad
feat: compare ast to string in is_equivalent
ojeytonwilliams Jan 15, 2024
be3e3fd
refactor: wrap tree in module for comparison
ojeytonwilliams Jan 15, 2024
2f951f3
feat: compare constants
ojeytonwilliams Jan 15, 2024
6bb5bdb
refactor: show that it can handle all expressions
ojeytonwilliams Jan 15, 2024
fa7d897
fix: TypeError if is_equivalent is called on None
ojeytonwilliams Jan 15, 2024
26e7095
refactor: simplify is_equivalent
ojeytonwilliams Jan 16, 2024
a2ba7d2
feat: return chainables list, not single chainable
ojeytonwilliams Jan 16, 2024
927b233
feat: add find_if_bodies helper
ojeytonwilliams Jan 16, 2024
f9cf98d
fix: handle printing of Chainable(None)
ojeytonwilliams Jan 16, 2024
00650ae
chore: remove unused functions
ojeytonwilliams Jan 16, 2024
2afde0b
fix: use Chainable(None) if variable not found
ojeytonwilliams Jan 16, 2024
9a461a9
feat: use is_integer after find_variable
ojeytonwilliams Jan 16, 2024
efd35b8
feat: change Chainable(None) handling
ojeytonwilliams Jan 16, 2024
e06e182
feat: add __eq__
ojeytonwilliams Jan 16, 2024
5eed0b5
eat: return Chainable() instead of None
ojeytonwilliams Jan 16, 2024
46fc16d
fix: handle unhappy path for find_function
ojeytonwilliams Jan 16, 2024
08d3c9b
fix: update other 'find's to handle other ASTs
ojeytonwilliams Jan 16, 2024
fa53522
fix: find_conditions when not called on if AST
ojeytonwilliams Jan 16, 2024
405538f
fix: has_function
ojeytonwilliams Jan 16, 2024
2d6d29d
refactor: minor tweaks
ojeytonwilliams Jan 16, 2024
2a25d2e
chore: use webpack to bundle py script
ojeytonwilliams Jan 22, 2024
211d1f5
chore: remove old prepare script
ojeytonwilliams Jan 22, 2024
f0a5648
feat: export astHelpers in python object
ojeytonwilliams Jan 22, 2024
24d6587
chore(deps): remove tsx
ojeytonwilliams Jan 22, 2024
1a38548
fix(workflow): be honest about the python version
ojeytonwilliams Jan 22, 2024
f70f6ef
fix: more honesty about versions
ojeytonwilliams Jan 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,19 @@ jobs:

- name: Run Tests
run: npm test

python-tests:
name: "Python CI"
runs-on: ubuntu-latest
steps:
# Checkout the Repo
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3

# Install Python 3.9
ojeytonwilliams marked this conversation as resolved.
Show resolved Hide resolved
- name: Setup Python
uses: actions/setup-python@v4 # v2
ojeytonwilliams marked this conversation as resolved.
Show resolved Hide resolved
with:
python-version: 3.x

- name: Run Tests
run: python ./python/py_helpers.test.py
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,7 @@ dist

# mdbook (docs)
book

# python
python/__pycache__

10 changes: 10 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('jest').Config} */
const config = {
moduleNameMapper: {
// We don't use the python scripts in the tests, so we can mock them with
// a simple script that does nothing.
"python/(.*.py)": "<rootDir>/python/mock-script.ts",
},
};

module.exports = config;
18 changes: 10 additions & 8 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { strip } from "./strip";
import astHelpers from "../python/py_helpers.py";

/**
* Removes every HTML-comment from the string that is provided
Expand Down Expand Up @@ -96,8 +97,9 @@ const getIsDeclaredAfter = (styleRule: CSSStyleRule) => (selector: string) => {
return currPosition > prevPosition;
};

export module python {
export function getDef(code: string, functionName: string) {
export const python = {
astHelpers,
getDef(code: string, functionName: string) {
const regex = new RegExp(
`\\n?(?<function_indentation> *?)def +${functionName} *\\((?<function_parameters>[^\\)]*)\\)\\s*: *?\\n(?<function_body> +.*?)(?=\\n\\k<function_indentation>[\\w#]|$)`,
"s"
Expand All @@ -122,11 +124,11 @@ export module python {
}

return null;
}
},

export function removeComments(code: string) {
removeComments(code: string) {
return code.replace(/\/\/.*|\/\*[\s\S]*?\*\/|(#.*$)/gm, "");
}
},

/**
* Gets a Python block of code matching the `blockPattern`
Expand All @@ -135,7 +137,7 @@ export module python {
*
* **Note:** A string `blockPattern` will be escaped to prevent special characters from being treated as regular expression syntax.
*/
export function getBlock(code: string, blockPattern: string | RegExp) {
getBlock(code: string, blockPattern: string | RegExp) {
const escapedBlockPattern =
blockPattern instanceof RegExp
? blockPattern.source
Expand All @@ -162,8 +164,8 @@ export module python {
}

return null;
}
}
},
};

export class CSSHelp {
doc: Document;
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions python/mock-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
161 changes: 161 additions & 0 deletions python/py_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import ast

# A chainable class that allows us to call functions on the result of parsing a string


class Chainable:
# TODO: allow initialization with a string
def __init__(self, tree=None):
self.tree = tree

def __getitem__(self, i):
if getattr(self.tree, "__getitem__", False):
return Chainable(self.tree[i])
else:
return Chainable(self.tree.body[i])

def __len__(self):
if getattr(self.tree, "__len__", False):
return len(self.tree)
else:
return len(self.tree.body)

def __eq__(self, other):
if not isinstance(other, Chainable):
return False
if self.tree == None:
return other.tree == None
if other.tree == None:
return False
return ast.dump(self.tree, include_attributes=True) == ast.dump(
other.tree, include_attributes=True
)

def __repr__(self):
if self.tree == None:
return "Chainable:\nNone"
return "Chainable:\n" + ast.dump(self.tree, indent=2)

def parse(self, string):
return Chainable(ast.parse(string))

def _has_body(self):
return bool(getattr(self.tree, "body", False))

# "find" functions return a new chainable with the result of the find
# function. In this case, it returns a new chainable with the function
# definition (if it exists)

def find_function(self, func):
if not self._has_body():
return Chainable()
for node in self.tree.body:
if isinstance(node, ast.FunctionDef):
if node.name == func:
return Chainable(node)
return Chainable()

# "has" functions return a boolean indicating whether whatever is being
# searched for exists. In this case, it returns True if the variable exists.

def has_variable(self, name):
return self.find_variable(name) != Chainable()

def find_variable(self, name):
if not self._has_body():
return Chainable()
for node in self.tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
if target.id == name:
return Chainable(node)
return Chainable()

def get_variable(self, name):
var = self.find_variable(name)
if var != Chainable():
return var.tree.value.value
else:
return None

def has_function(self, name):
return self.find_function(name) != Chainable()

# Checks the variable, name, is in the current scope and is an integer

def is_integer(self):
if not isinstance(self.tree, ast.Assign):
return False
return type(self.tree.value.value) == type(1)

def value_is_call(self, name):
if not isinstance(self.tree, ast.Assign):
return False
call = self.tree.value
if isinstance(call, ast.Call):
return call.func.id == name
return False

# Takes an string and checks if is equivalent to the chainable's AST. This
# is a loose comparison that tries to find out if the code is essentially
# the same. For example, the string "True" is not represented by the same
# AST as the test in "if True:" (the string could be wrapped in Module,
# Interactive or Expression, depending on the parse mode and the test is
# just a Constant), but they are equivalent.

def is_equivalent(self, target_str):
# Setting the tree to None is used to represent missing elements. Such
# as the condition of a final else clause. It is, therefore, not
# equivalent to any string.
if self.tree == None:
return False
return ast.unparse(self.tree) == ast.unparse(ast.parse(target_str))

# Finds the class definition with the given name

def find_class(self, class_name):
if not self._has_body():
return Chainable()
for node in self.tree.body:
if isinstance(node, ast.ClassDef):
if node.name == class_name:
return Chainable(node)
return Chainable()

# Find an array of conditions in an if statement

def find_ifs(self):
return self._find_all(ast.If)

def _find_all(self, ast_type):
return [
Chainable(node) for node in self.tree.body if isinstance(node, ast_type)
]

def find_conditions(self):
def _find_conditions(tree):
if not isinstance(tree, ast.If):
return []
test = tree.test
if self.tree.orelse == []:
return [test]
elif isinstance(tree.orelse[0], ast.If):
return [test] + _find_conditions(tree.orelse[0])
else:
return [test, None]

return [Chainable(test) for test in _find_conditions(self.tree)]

# Find an array of bodies in an elif statement

def find_if_bodies(self):
def _find_if_bodies(tree):
if self.tree.orelse == []:
return [tree.body]
elif isinstance(tree.orelse[0], ast.If):
return [tree.body] + _find_if_bodies(tree.orelse[0])
else:
return [tree.body] + [tree.orelse]

return [Chainable(body) for body in _find_if_bodies(self.tree)]
Loading
Loading