-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c08d5cf
commit 6843aa4
Showing
4 changed files
with
162 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
# -*- coding: utf-8 -*- | ||
# <sure - sophisticated automated test library and runner> | ||
# Copyright (C) <2010-2024> Gabriel Falcão <[email protected]> | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
"""astuneval (Abstract Syntax-Tree Unevaluation) - safe substitution for unsafe :func:`eval` | ||
""" | ||
import ast | ||
|
||
|
||
class Accessor(object): | ||
"""base class for object element accessors""" | ||
|
||
def __init__(self, astbody): | ||
self.body = astbody | ||
|
||
def __call__(self, object: object, *args, **kw) -> object: | ||
return self.access(object, *args, **kw) | ||
|
||
def access(self, object: object) -> object: | ||
raise NotImplementedError(f"support to {type(self.body)} is not implemented") | ||
|
||
|
||
class NameAccessor(Accessor): | ||
"""Accesses an object's attributes through name""" | ||
|
||
def access(self, object: object) -> object: | ||
return getattr(object, self.body.id) | ||
|
||
|
||
class SliceAccessor(Accessor): | ||
"""Accesses an object's attributes through slice""" | ||
|
||
def access(self, object: object) -> object: | ||
return object[self.body.value] | ||
|
||
|
||
class SubsAccessor(Accessor): | ||
"""Accesses an object's attributes through subscript""" | ||
|
||
def access(self, object: object) -> object: | ||
get_value = NameAccessor(self.body.value) | ||
get_slice = SliceAccessor(self.body.slice) | ||
return get_slice(get_value(object)) | ||
|
||
|
||
class AttributeAccessor(Accessor): | ||
"""Accesses an object's attributes through chained attribute""" | ||
|
||
def access(self, object: object) -> object: | ||
attr_name = self.body.attr | ||
access = resolve_accessor(self.body.value) | ||
value = access(object) | ||
return getattr(value, attr_name) | ||
|
||
|
||
def resolve_accessor(body): | ||
return { | ||
ast.Name: NameAccessor, | ||
ast.Subscript: SubsAccessor, | ||
ast.Attribute: AttributeAccessor, | ||
}.get(type(body), Accessor)(body) | ||
|
||
|
||
def parse_accessor(value: str) -> Accessor: | ||
body = parse_body(value) | ||
return resolve_accessor(body) | ||
|
||
|
||
def parse_body(value: str) -> ast.stmt: | ||
bodies = ast.parse(value).body | ||
if len(bodies) > int(True): | ||
raise SyntaxError(f"{repr(value)} exceeds the maximum body count for ast nodes") | ||
|
||
return bodies[0].value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# -*- coding: utf-8 -*- | ||
# <sure - sophisticated automated test library and runner> | ||
# Copyright (C) <2010-2024> Gabriel Falcão <[email protected]> | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
import ast | ||
from sure import expects | ||
from sure.astuneval import parse_body | ||
from sure.astuneval import parse_accessor | ||
from sure.astuneval import Accessor, NameAccessor, SubsAccessor, AttributeAccessor | ||
|
||
|
||
def test_parse_body_against_several_kinds(): | ||
expects(parse_body("atomic_bonds[3:7]")).to.be.an(ast.Subscript) | ||
expects(parse_body("children[6]")).to.be.an(ast.Subscript) | ||
expects(parse_body("hippolytus")).to.be.an(ast.Name) | ||
expects(parse_body("zone[4].damage")).to.be.an(ast.Attribute) | ||
|
||
|
||
def test_parse_accessor_name_accessor(): | ||
class Tragedy: | ||
telemachus = "♒️" | ||
|
||
expects(parse_accessor("telemachus")).to.be.a(NameAccessor) | ||
get_character = parse_accessor("telemachus") | ||
expects(get_character(Tragedy)).to.equal('♒️') | ||
|
||
|
||
def test_parse_accessor_subscript_accessor(): | ||
class MonacoGrandPrix1990: | ||
classification = [ | ||
"Ayrton Senna", | ||
"Alain Prost", | ||
"Jean Alesi", | ||
] | ||
expects(parse_accessor("classification[2]")).to.be.a(SubsAccessor) | ||
get_position = parse_accessor("classification[2]") | ||
expects(get_position(MonacoGrandPrix1990)).to.equal("Jean Alesi") | ||
|
||
|
||
def test_parse_accessor_attr_accessor(): | ||
class FirstResponder: | ||
def __init__(self, bound: str, damage: str): | ||
self.bound = bound | ||
self.damage = damage | ||
|
||
class Incident: | ||
first_responders = [ | ||
FirstResponder("Wyckoff", "unknown"), | ||
FirstResponder("Beth Israel", "unknown"), | ||
FirstResponder("Brooklyn Hospital Center", "unknown"), | ||
FirstResponder("Woodhull", "administered wrong medication"), | ||
] | ||
expects(parse_accessor("first_responders[3].damage")).to.be.a(AttributeAccessor) | ||
|
||
access_damage = parse_accessor("first_responders[3].damage") | ||
expects(access_damage(Incident)).to.equal("administered wrong medication") |