Skip to content
This repository has been archived by the owner on Oct 7, 2023. It is now read-only.

Commit

Permalink
more test cases; bump to v0.1.6 (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshKarpel authored Nov 15, 2020
1 parent 097b547 commit d24c07f
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 60 deletions.
48 changes: 27 additions & 21 deletions dis_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from dataclasses import dataclass
from pathlib import Path
from types import FunctionType, ModuleType
from typing import Dict, Iterable, List, Optional, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union

import click
from rich.color import ANSI_COLOR_NAMES
Expand Down Expand Up @@ -103,7 +103,8 @@ def find_function(target: str) -> FunctionType:

if len(parts) == 1:
try:
raise target_was_a_module(silent_import(parts[0]))
module = silent_import(parts[0])
raise bad_target(target, module, module)
except ModuleNotFoundError as e:
# target was not *actually* a module
raise click.ClickException(str(e))
Expand All @@ -114,26 +115,26 @@ def find_function(target: str) -> FunctionType:
module_path, target_path = ".".join(parts[:split_point]), ".".join(parts[split_point:])

try:
obj = silent_import(module_path)
module = obj = silent_import(module_path)
break
except ModuleNotFoundError:
pass

for o in target_path.split("."):
for target_path_part in target_path.split("."):
try:
obj = getattr(obj, o)
obj = getattr(obj, target_path_part)
except AttributeError:
raise click.ClickException(
f"No attribute named {o!r} found on {type(obj).__name__} {obj!r}."
f"No attribute named {target_path_part!r} found on {type(obj).__name__} {obj!r}."
)

if inspect.ismodule(obj):
raise target_was_a_module(obj)

# If the target is a class, display its __init__ method
if inspect.isclass(obj):
obj = obj.__init__ # type: ignore

if not inspect.isfunction(obj):
raise bad_target(target, obj, module)

return obj


Expand All @@ -151,19 +152,24 @@ def silent_import(module_path: str) -> ModuleType:
)


def target_was_a_module(module: ModuleType) -> click.ClickException:
possibilities = list(find_functions(module))
def bad_target(target: str, obj: Any, module: ModuleType) -> click.ClickException:
possible_targets = find_possible_targets(module)

if len(possibilities) == 0:
return click.ClickException(f"Cannot disassemble modules. Target a specific function.")
choice = random.choice(possibilities)
suggestion = click.style(f"{choice.__module__}.{choice.__qualname__}", bold=True)
return click.ClickException(
f"Cannot disassemble modules. Target a specific function, like {suggestion}"
)
msg = f"The target {target} = {obj} is a {type(obj).__name__}, which cannot be disassembled. Target a specific function"

if len(possible_targets) == 0:
return click.ClickException(f"{msg}.")
else:
choice = random.choice(possible_targets)
suggestion = click.style(f"{choice.__module__}.{choice.__qualname__}", bold=True)
return click.ClickException(f"{msg}, like {suggestion}")


def find_possible_targets(obj: ModuleType) -> List[FunctionType]:
return list(_find_possible_targets(obj))


def find_functions(
def _find_possible_targets(
module: ModuleType, top_module: Optional[ModuleType] = None
) -> Iterable[FunctionType]:
for obj in vars(module).values():
Expand All @@ -175,7 +181,7 @@ def find_functions(
if inspect.isfunction(obj):
yield obj
elif inspect.isclass(obj):
yield from find_functions(obj, top_module=top_module or module)
yield from _find_possible_targets(obj, top_module=top_module or module)


def make_source_and_bytecode_display(function: FunctionType, theme: str) -> Display:
Expand Down Expand Up @@ -207,7 +213,7 @@ def make_source_and_bytecode_display(function: FunctionType, theme: str) -> Disp
renderables=(line_numbers_block, source_block, line_numbers_block, bytecode_block)
),
],
height=len(code_lines) + 1, # the 1 is from the Rule
height=max(len(code_lines), len(instruction_rows)) + 1, # the 1 is from the Rule
)


Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = dis_cli
version = 0.1.5
version = 0.1.6
description = A tool to inspect disassembled Python code on the command line.
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down Expand Up @@ -29,7 +29,7 @@ classifiers =
py_modules = dis_cli
install_requires =
click>=7
rich>=7
rich>=9
dataclasses>=0.6;python_version<"3.7"
importlib-metadata;python_version<"3.8"
importlib-resources;python_version<"3.7"
Expand Down
90 changes: 53 additions & 37 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,50 @@ def test_smoke(cli):
assert cli(["dis.dis"]).exit_code == 0


ATTR_NAME = "wiz"
CLASS_NAME = "Foo"
CONST_NAME = "BANG"
FUNC_NAME = "func"
FUNC = f"""
METHOD_NAME = "bar"
NONE_NAME = "noon"

SOURCE = f"""
{CONST_NAME} = "string"
{NONE_NAME} = None
def {FUNC_NAME}():
pass
"""
CLASS_NAME = "Foo"
METHOD_NAME = "bar"
CLASS = f"""
class {CLASS_NAME}:
{ATTR_NAME} = True
def {METHOD_NAME}(self):
pass
"""


@pytest.fixture
def source_path_with_func(test_dir, filename) -> Path:
source_path = test_dir / f"{filename}.py"
source_path.write_text(FUNC)

return source_path


@pytest.fixture
def source_path_with_class(test_dir, filename) -> Path:
def source_path(test_dir, filename) -> Path:
source_path = test_dir / f"{filename}.py"
source_path.write_text(CLASS)
source_path.write_text(SOURCE)

return source_path


def test_runs_successfully_on_func_source(cli, source_path_with_func):
result = cli([f"{source_path_with_func.stem}.{FUNC_NAME}"])
def test_runs_successfully_on_func_source(cli, source_path):
result = cli([f"{source_path.stem}.{FUNC_NAME}"])

assert result.exit_code == 0


def test_func_source_in_output(cli, source_path_with_func):
result = cli([f"{source_path_with_func.stem}.{FUNC_NAME}"])
def test_func_source_in_output(cli, source_path):
result = cli([f"{source_path.stem}.{FUNC_NAME}"])

assert all([line in result.output for line in FUNC.splitlines()])
assert f"def {FUNC_NAME}():" in result.output


def test_handle_missing_target_gracefully(cli, source_path_with_func):
result = cli([f"{source_path_with_func.stem}.{FUNC_NAME}osidjafoa"])
def test_handle_missing_target_gracefully(cli, source_path):
result = cli([f"{source_path.stem}.{FUNC_NAME}osidjafoa"])

assert result.exit_code == 1
assert "osidjafoa" in result.output
Expand All @@ -66,7 +65,7 @@ def test_handle_missing_target_gracefully(cli, source_path_with_func):
)
def test_module_level_output_is_not_shown(cli, test_dir, filename, extra_source):
source_path = test_dir / f"{filename}.py"
source_path.write_text(f"{extra_source}\n{FUNC}")
source_path.write_text(f"{extra_source}\n{SOURCE}")
print(source_path.read_text())

result = cli([f"{source_path.stem}.{FUNC_NAME}"])
Expand All @@ -78,7 +77,7 @@ def test_module_level_output_is_not_shown(cli, test_dir, filename, extra_source)
@pytest.mark.parametrize("extra_source", ["raise Exception", "syntax error"])
def test_module_level_error_is_handled_gracefully(cli, test_dir, filename, extra_source):
source_path = test_dir / f"{filename}.py"
source_path.write_text(f"{extra_source}\n{FUNC}")
source_path.write_text(f"{extra_source}\n{SOURCE}")
print(source_path.read_text())

result = cli([f"{source_path.stem}.{FUNC_NAME}"])
Expand All @@ -87,7 +86,7 @@ def test_module_level_error_is_handled_gracefully(cli, test_dir, filename, extra
assert "during import" in result.output


def test_targetting_a_class_redirects_to_init(cli, test_dir, filename):
def test_targeting_a_class_redirects_to_init(cli, test_dir, filename):
source_path = test_dir / f"{filename}.py"
source_path.write_text(
textwrap.dedent(
Expand All @@ -106,6 +105,22 @@ def __init__(self):
assert "foobar" in result.output


def test_can_target_method(cli, source_path):
result = cli([f"{source_path.stem}.{CLASS_NAME}.{METHOD_NAME}"])

assert result.exit_code == 0
assert METHOD_NAME in result.output


def test_module_not_found(cli):
target = "fidsjofoiasjoifdj"
result = cli([target])

assert result.exit_code == 1
assert "No module named" in result.output
assert target in result.output


@pytest.mark.parametrize(
"target",
[
Expand All @@ -117,22 +132,23 @@ def test_gracefully_cannot_disassemble_module(cli, target):
result = cli([target])

assert result.exit_code == 1
assert "Cannot disassemble modules" in result.output


def test_can_target_method(cli, source_path_with_class):
result = cli([f"{source_path_with_class.stem}.{CLASS_NAME}.{METHOD_NAME}"])

assert result.exit_code == 0
assert "cannot be disassembled" in result.output
assert "module" in result.output


def test_module_not_found(cli):
target = "fidsjofoiasjoifdj"
result = cli([target])
@pytest.mark.parametrize(
"target",
[
f"{CONST_NAME}",
f"{CLASS_NAME}.{ATTR_NAME}",
f"{NONE_NAME}",
],
)
def test_cannot_be_disassembled(cli, source_path, target):
result = cli([f"{source_path.stem}.{target}"])

assert result.exit_code == 1
assert "No module named" in result.output
assert target in result.output
assert "cannot be disassembled" in result.output


def test_version(cli):
Expand Down

0 comments on commit d24c07f

Please sign in to comment.