Created
January 7, 2025 22:40
-
-
Save alexbowe/b42d14e6115baa7b04e34badf837e0c5 to your computer and use it in GitHub Desktop.
Diff Python objects by dotted path
This file contains hidden or 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
import inspect | |
import argparse | |
from importlib import import_module | |
from difflib import unified_diff | |
# If Rich is available, use it to colorize the diff. | |
# NOTE: Users can also use other tools like `colordiff` | |
try: | |
from rich.console import Console | |
from rich.syntax import Syntax | |
_RICH_AVAILABLE = True | |
except ImportError: | |
_RICH_AVAILABLE = False | |
def import_object(dotted_str): | |
""" | |
Attempt to import the longest valid module prefix. | |
Then get the remaining attributes nested in that module. | |
Example: | |
dotted_str = "mypkg.mymodule.SomeClass.NestedClass.my_method" | |
""" | |
parts = dotted_str.split(".") | |
for i in reversed(range(1, len(parts) + 1)): | |
try: | |
obj = import_module(".".join(parts[:i])) | |
for attr in parts[i:]: | |
obj = getattr(obj, attr) | |
return obj | |
except ImportError: | |
pass | |
raise ImportError(f"Cannot import '{dotted_str}'") | |
def get_source(dotted_str): | |
obj = import_object(dotted_str) | |
return inspect.getsource(obj) | |
def parse_args(): | |
parser = argparse.ArgumentParser( | |
description="Diff two Python objects via dotted paths" | |
) | |
parser.add_argument("obj1", help="e.g. mypkg.mymodule.SomeClass.my_method") | |
parser.add_argument("obj2", help="e.g. mypkg2.mymodule2.OtherClass.other_method") | |
return parser.parse_args() | |
def diff_objects(obj_path1, obj_path2): | |
source1 = get_source(obj_path1) | |
source2 = get_source(obj_path2) | |
diff_lines = unified_diff( | |
source1.splitlines(), | |
source2.splitlines(), | |
fromfile=obj_path1, | |
tofile=obj_path2, | |
) | |
return diff_lines | |
def print_diff(diff_lines): | |
diff_text = "\n".join(diff_lines) | |
if _RICH_AVAILABLE: | |
console = Console(force_terminal=True) | |
syntax = Syntax(diff_text, "diff", theme="ansi_dark", line_numbers=False) | |
console.print(syntax) | |
else: | |
print(diff_text) | |
def main(): | |
args = parse_args() | |
obj_path1, obj_path2 = args.obj1, args.obj2 | |
diff_lines = diff_objects(obj_path1, obj_path2) | |
print_diff(diff_lines) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment