GithubHelp home page GithubHelp logo

gaogaotiantian / objprint Goto Github PK

View Code? Open in Web Editor NEW
489.0 6.0 43.0 90 KB

A library that can print Python objects in human readable format

License: Apache License 2.0

Makefile 0.51% Python 99.49%
debug python3 python print

objprint's Introduction

objprint

build coverage pypi support-version license commit

A library that can print Python objects in human readable format

Install

pip install objprint

Usage

op

Use op() (or objprint()) to print objects.

from objprint import op

class Position:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Player:
    def __init__(self):
        self.name = "Alice"
        self.age = 18
        self.items = ["axe", "armor"]
        self.coins = {"gold": 1, "silver": 33, "bronze": 57}
        self.position = Position(3, 5)

op(Player())
<Player 0x7fe44e1e3070
  .age = 18,
  .coins = {'bronze': 57, 'gold': 1, 'silver': 33},
  .items = ['axe', 'armor'],
  .name = 'Alice',
  .position = <Position
    .x = 3,
    .y = 5
  >
>

You can print multiple objects just like print, except op will print them in separate lines

op([1, 2], {'a': 1})
[1, 2]
{'a': 1}

op will return the same object it prints, so you can do something like this

a = MyObject()
# print the args inline with minumum change
function_using_object(op(a))
# the difference is more significant with complex expressions
# original: function_using_object(a.f() + a.g())
function_using_object(op(a.f() + a.g()))

It works on multiple objects as well, as it returns a tuple, you need to unpack it for functions

a = MyObject()
function_using_object(*op(a.f(), a.g()))

add_objprint

If you want to use print() to print your object, you can also use the class decorator add_objprint to add __str__ method for your class.

from objprint import add_objprint

class Position:
    def __init__(self, x, y):
        self.x = x
        self.y = y

@add_objprint
class Player:
    def __init__(self):
        self.name = "Alice"
        self.age = 18
        self.items = ["axe", "armor"]
        self.coins = {"gold": 1, "silver": 33, "bronze": 57}
        self.position = Position(3, 5)

# This will print the same thing as above
print(Player())

objstr

If you want the str representation of the object, instead of printing it on the screen, you can use objstr function

from objprint import objstr

s = objstr(my_object)

print more

There are some optional information you can print with config.

"Public" Methods

There are no REAL public methods in python, here I simply meant you can print methods that do not start with __ as there will be a lot of default magic methods and you don't want that.

class Player:
    def attack(self, opponent):
        pass

op(Player(), print_methods=True)
<Player 0x7fe44e1e3070
  def attack(opponent)
>

As you can see, it will also print the method signature(without self argument).

Line numbers

You can print execution info, including the function it's in, the file and the line number of the printing line. This is helpful for you to locate where this object is printed.

def f():
    op(Player(), line_number=True)
f()
f (my_script.py:29)
<Player 0x7f30e8cb1ac0
  ...
>

Argument names

You can print the expression of the argument with arg_name

op(Player(), arg_name=True)
Player():
<Player 0x7f495850a8d0
  ...
>

objjson

objprint supports print objects to json to make it easier to serialize an object.

objjson returns a jsonifiable object that can be dumped with json.dumps

from objprint import objjson

json_obj = objjson(Player())

print(json.dumps(json_obj, indent=2))
{
  ".type": "Player",
  "name": "Alice",
  "age": 18,
  "items": [
    "axe",
    "armor"
  ],
  "coins": {
    "gold": 1,
    "silver": 33,
    "bronze": 57
  },
  "position": {
    ".type": "Position",
    "x": 3,
    "y": 5
  }
}

You can use op to print in json format directly with format="json". You can pass in argument for json.dumps

op(Player(), format="json", indent=2)

add_objprint also works with format="json"

@add_objprint(format="json", indent=2)
class Player:
    pass

Enable/Disable the print

You can disable prints from all the op() calls globally with enable config.

from objprint import op

op.disable()
op([1, 2, 3])  # This won't print anything
op.enable()  # This could fix it!

Or you can use it for op() functions individually with some conditions

op(obj, enable=check_do_print())

attribute selection

You can customize which attribute to print with name filters.

objprint will try to match the attribute name with attr_pattern regex. The default attr_pattern is r"(!_).*", which means anything that does NOT start with an _.

You can customize attr_pattern to select the attributes you want to print:

# This will print all the attributes that do not start with __
op(Player(), attr_pattern=r"(!__).*")

You can also use include and exclude to specify attributes to print with regular expression so objprint will only print out the attributes you are interested in.

op(Player(), include=["name"])
<Player
  .name = 'Alice'
>
op(Player(), exclude=[".*s"])
<Player 0x7fe44e1e3070
  .name = 'Alice',
  .age = 18,
  .position = <Position
    .x = 3,
    .y = 5
  >
>

If you specify both include and exclude, it will do a inclusive check first, then filter out the attributes that match exclusive check.

attr_pattern, include and exclude arguments work on objprint, objstr and @add_objprint.

Register Custom Type Formatter

You can also customize how certain types of objects are displayed by registering a custom formatter function to transform an object of a specific type into a string.

For example, you can print all integers in hexadecimal format by registering the hex() function for the int data type, or registering a custom lambda function.

from objprint import op

op.register_formatter(int, hex)
op.register_formatter(float, lambda x: f"{round(x, 3)}")
op(10)  # prints 0xa
op(3.14159)  # prints 3.142

Alternatively, you can also register a custom formatter function using a decorator:

@op.register_formatter(str)
def custom_formatter(obj: str):
    return f"custom_print: {obj}"

op("hi")  # prints custom_print: hi

During registration, objprint will examine the specified object type, and raise a TypeError if an invalid object type is provided.

When you finish using the custom formatters, you can unregister them with unregister_formatter().

op.unregister_formatter(int, float, str)
op(10)  # prints 10
op(3.14159)  # prints 3.14159
op("hi")  # prints hi

Or you can unregister everything by passing no argument to it.

op.unregister_formatter()

The register_formatter() function also accepts an inherit argument (default True) to dictate if the registered formatter should also apply to any derived classes of the object type.

class BaseClass:
    name = 'A'

class DerivedClass(BaseClass):
    name = 'B'

With inherit=True, derived class will share the same formatter registered under base class.

def base_formatter(obj: BaseClass) -> str:
    return f'Print {obj.name} with Base Class Formatter'

op.register_formatter(BaseClass, base_formatter, inherit=True)

op(DerivedClass())
Print B with Base Class Formatter

With inherit=False, derived class will use the default formatter provided by objprint.

@op.register_formatter(BaseClass, inherit=False)
def base_formatter(obj: BaseClass) -> str:
    return f'Print {obj.name} with Base Class Formatter'

op(DerivedClass())
<DerivedClass 0x7fb42e8216a0
  .name = 'B'
>

If a derived class inherits from multiple base classes, each with a registered formatter, the chosen formatter adheres to the Method Resolution Order (MRO) of the derived class.

To check all the registered functions and their inheritance status, you can use the get_formatter() method. It returns a dictionary-like object that you can print for easy inspection.

fmts = op.get_formatter()
print(fmts)
{<class '__main__.BaseClass'>: FormatterInfo(formatter=<function base_formatter at 0x7feaf33d1f70>, inherit=False)}

Please note that registering a formatter function with op will affect the output of objprint and objstr methods in the same way.

config

objprint formats the output based on some configs

  • config_name(default_value) - this config's explanation
  • enable(True) - whether to print, it's like a switch
  • depth(100) - how deep objprint goes into nested data structures
  • indent(2) - the indentation
  • width(80) - the maximum width a data structure will be presented as a single line
  • elements(-1) - the maximum number of elements that will be displayed, -1 means no restriction
  • color(True) - whether to use colored scheme
  • line_number(False) - whether to print the function (filename:line_number) before printing the object
  • arg_name(False) - whether to print the argument expression before the argument value
  • skip_recursion(True) - whether skip printing recursive data, which would cause infinite recursion without depth constraint
  • honor_existing(True) - whether to use the existing user defined __repr__ or __str__ method
  • attr_pattern(r"(!_).*") - the regex pattern for attribute selection
  • include([]) - the list of attribute regex to do an inclusive filter
  • exclude([]) - the list of attribute regex to do an exclusive filter

You can set the configs globally using config function

from objprint import config
config(indent=4)

Or if you don't want to mess up your name space

from objprint import op
op.config(indent=4)

Or you can do a one time config by passing the arguments into objprint function

from objprint import op

op(var, indent=4)

install

Maybe you don't want to import op in every single file that you want to use. You can use install to make it globally accessible

from objprint import op, install

# Now you can use op() in any file
install()

# This is the same
op.install()

# You can specify a name for objprint()
install("my_print")
my_print(my_object)

Bugs/Requests

Please send bug reports and feature requests through github issue tracker.

License

Copyright 2020-2023 Tian Gao.

Distributed under the terms of the Apache 2.0 license.

objprint's People

Contributors

carinaqyy avatar gaogaotiantian avatar happytree718 avatar in-the-ocean avatar jackiezc086 avatar ovizro avatar zyckk4 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

objprint's Issues

如何实现“增量”打印?

您好!我正在使用objprint跟踪迭代过程,代码大致如下:

for i in range(i_max):
    obj.step()
    op(obj)

已知obj中有些变量在执行step后是不发生改变的,在第二次到最后一次调用op时,是否能够不打印这些变量?也就是只打印发生了变化的变量?

`arg_name=True` not usable in Jupyter & IPython

from objprint import op

def func():
    return 1

cc = op(func(), arg_name=True)

image

maybe it's because here:

module = inspect.getmodule(frame)
if module is None:
return None
source = inspect.getsource(module)

module is None for Jupyter & IPython, but they have file:
jupyter:
image

ipython:
image

and source can be retrieved with:
image

Change to source = inspect.getsource(frame) will work:
image

在打印链表的时候结构不是很清晰

打印链表会输出

<Node 0x18136a4bd30
  .next = <Node 0x18136a4b3d0
    .next = <Node 0x18136dd5750
      .next = <Node 0x18136dd5930
        .next = <Node 0x18136dd5810
          .next = <Node 0x18136dd5870
            .next = None,
            .value = 'node_5'
          >,
          .value = 'node_4'
        >,
        .value = 'node_3'
      >,
      .value = 'node_2'
    >,
    .value = 'node_1'
  >,
  .value = 'Head'
>

可以考虑在打印属性为对象的属性的时候缩进幅度大一些

<Node 0x18136a4bd30
    .next = <Node 0x18136a4b3d0
            .next = <Node 0x18136dd5750
                    .next = <Node 0x18136dd5930
                                .next = <Node 0x18136dd5810
                                        .next = <Node 0x18136dd5870
                                                .next = None,
                                                .value = 'node_5'
                                        >,
                                        .value = 'node_4'
                                >,
                                .value = 'node_3'
                    >,
                    .value = 'node_2'
            >,
            .value = 'node_1'
     >,
     .value = 'Head'
>

或者结构修改一下

<Node 0x18136a4bd30 ->  .next = <Node 0x18136a4b3d0 ->  .next = <Node 0x18136dd5750 ->  .next = <Node 0x18136dd5930 ->  .next = <Node 0x18136dd5810 ->  .next = <Node 0x18136dd5870 ->  .next = None,
                                                                                                                                                                                        .value = 'node_5'
                                                                                                                                                                                        >,         
                                                                                                                                                        .value = 'node_4'
                                                                                                                                                        >,
                                                                                                                        .value = 'node_3'
                                                                                                                        >,
                                                                                        .value = 'node_2'
                                                                                        >,
                                                        .value = 'node_1'
                                                        >,
                        .value = 'Head'
                        >

简单的建议,希望能优化一下!

Feature: Improve printing of code object

Reprodution

Run the script below:

from objprint import op
def _f():
    pass
op(_f.__code__)

It will give a output like this: <code object _f at 0x00000222E40EF100, file "C:\Users\user\AppData\Roaming\JetBrains\PyCharmCE2023.2\scratches\scratch.py", line 2>

Excepted behavior

objprint should show all the attributes in the code object.

[Feature Request] Limit number of rows displayed for large arrays/dictionaries

Hello,

I hope you're doing well. I would like to propose a feature enhancement that I believe could improve the user experience when dealing with large arrays or dictionaries. Currently, when using objprint to print an object with a large array or dictionary, it displays all the elements, making the output lengthy and overwhelming.

It would be great to have an optional argument that allows us to limit the number of rows displayed for arrays or dictionaries, similar to how pandas handles DataFrame display. By default, pandas shows 5 rows at the beginning and 5 at the end, with an ellipsis (…) in between to indicate that more rows are not shown. This approach gives users an idea of what's inside the data structure without cluttering the terminal output.

For instance, consider the following example:

from objprint import op, install

class Sample:
    def __init__(self, dict) -> None:
        self.data = dict


test_dict = {i:i for i in range(100)}

_ = op(Sample(test_dict))

In this case, objprint would print all 100 key-value pairs in self.data, making the output very long. It would be beneficial to have an option to display only the first and last few lines of the dictionary.

If you think this feature would be a valuable addition to the library, I would be more than happy to contribute to its implementation. Please let me know your thoughts on this suggestion.

Thank you for your time and consideration! Looking forward to hearing from you.

Best Regards,
Erwin

Use a generic class to test, instead of having a class for each test

@Carinaqyy

We have too many classes in our tests now. It makes the test file unnecessary long and it separates the test and the data it was using. For most tests, we only need a class with specific attributes. We should create a generic class that could be constructed with either a dict or arguments, and have those attributes set.

This way, we will have the data close to the actual test, and make the test file less ugly.

Detect repl for returning objects

Now op() returns the object it prints to make calling chain possible. But in repl, that will print two instances. We should fix that for a better user experience.

在面对Enum对象时,输出的结果不符合预期。

测试代码:

from enum import Enum
from objprint import op,add_objprint

class temp_a(Enum):
    a = 1

@add_objprint
class temp_b(Enum):
    b = 1

print("temp_a:\n",temp_a,"\n")

print("temp_a.a:\n",temp_a.a,"\n")

print("op_temp_a:")
op(temp_a)
print("\n")

print("op_temp_a.a:")
op(temp_a.a)
print("\n")

print("================================================================\n")

print("temp_b:\n",temp_b,"\n")

print("temp_b.b:\n",temp_b.b,"\n")

print("op_temp_b:")
op(temp_b)
print("\n")

print("op_temp_b.b:")
op(temp_b.b)
print("\n")

输出:

temp_a:
 <enum 'temp_a'> 

temp_a.a:
 temp_a.a

op_temp_a:
<enum 'temp_a'>


op_temp_a.a:
temp_a.a


================================================================

temp_b:
 <enum 'temp_b'>

temp_b.b:
 <temp_b 0x24a740c2ee0
  .__objclass__ = <enum 'temp_b'>,
  ._name_ = 'b',
  ._value_ = 1
>

op_temp_b:
<enum 'temp_b'>


op_temp_b.b:
<temp_b 0x24a740c2ee0
  .__objclass__ = <enum 'temp_b'>,
  ._name_ = 'b',
  ._value_ = 1
>

符合直觉的结果应该是:

class temp_a(Enum):
    a = 1
op(temp_a)

输出:

 <temp_a 0xFFFFFFFF
  .__objclass__ = <enum 'temp_a'>,
  .temp_a.a = < xxxxxxx
      ._name_ = 'b',
      ._value_ = 1
  >
>

pydantic.BaseModel 对象无法正确打印

我在使用 pydantic + objprint,打印对象使用了 #52 中的方法,但程序在 op 处阻塞住。

from objprint import op
from pydantic import BaseModel


class A(BaseModel):
    a: int


obj = A(a=1)
op(obj, honor_existing=False)

Also print the actual expression whose value is being printed

The https://github.com/gruns/icecream lib has an useful feature where it also prints the expression. Please see the following example:

d = {'key': {1: 'one'}}
ic(d['key'][1])

class klass():
    attr = 'yep'
ic(klass.attr)

It prints:

ic| d['key'][1]: 'one'
ic| klass.attr: 'yep'

As you can see icecream lib also prints the expression d['key'][1]. Is it possible to add this feature? This is the only feature holding me back from switching to objprint.

call_frame in objstr function seems not used

def objstr(self, obj: Any, call_frame: Optional[FrameType] = None, **kwargs) -> str:
if call_frame is not None:
call_frame = inspect.currentframe()
if call_frame is not None:
call_frame = call_frame.f_back
# If no color option is specified, don't use color
if "color" not in kwargs:
kwargs["color"] = False
cfg = self._configs.overwrite(**kwargs)
memo: Optional[Set[int]] = set() if cfg.skip_recursion else None
return self._objstr(obj, memo, indent_level=0, cfg=cfg)

Use colored scheme when printing to console

To make it more clear, we probably could use ANSI escape sequence to apply colors to something when printing to console. Make this configurable so the user can still have pure text experience.

Add more tests to fix_re_match branch

@Carinaqyy , we need more tests for fix_re_match.

  • Make sure include/exclude uses "full match". For example. ".*s" should match "coins", but not "position".
  • Add more tests using objprint, maybe create a new file. We can't only test objstr and assume it would work with objprint.
  • Restructure the test files. test_config should test the function config, not objprint with arguments.

Duplicate output when op() argument wasn't an object.

import hashlib
op(hashlib)
<module 'hashlib' from 'C:\Users\guojoan\AppData\Local\Programs\Python\Python310\lib\hashlib.py'>
<module 'hashlib' from 'C:\Users\guojoan\AppData\Local\Programs\Python\Python310\lib\hashlib.py'>

a = []
a.append(a)
op(a)
[[ ... ]]
[[...]]

and so on

[BUG] 未抑制读取属性时发生的异常

这是我在使用它打印sqlalchemy的一些对象时发现的问题。
最简单的用例如下:

from objprint import op


class Test:
    __slots__ = ["a"]


op(Test())

我获得了一个AttributeError: a。这并不是我所预期的,我认为更好的处理方式是ObjPrint抑制异常并忽略此属性。

在pycharm中没打印出来

image
我在我的pycharm中的terminal中运行,好像打印结果不对,不知道是不是我的用法不对还是有bug

print to json

It would be useful to print a Python object to json format that can be somehow displayed in HTML or parsed for other usage.

Feature: Partially setting the honor_existing option

我不确定这是不是个普遍的场景,还是算作特殊的需求。如下面的代码所示,我想用 objprint 来打印 Struct 类型的变量。但是因为 dataclass 已经有了 __str__ 的实现,我只能把 honor_existing 参数设为 False。可是这样 pathlib.Path 中 parent 属性会让 objprint 在输出时陷入无限递归,直到 depth。然而实际上我不需要对 pathlib.Path 的 honor_existing 设为 False。

from dataclasses import dataclass
from pathlib import Path

from objprint import objprint


@dataclass
class Struct:
    path: Path = Path("~")


if __name__ == "__main__":
    objprint(Struct(), honor_existing=False, depth=5)

image


我当前是通过加上自定义的 Formatter 实现上面的需求

from dataclasses import dataclass
from pathlib import Path

from objprint import objprint


@dataclass
class Struct:
    path: Path = Path("~")


if __name__ == "__main__":
    objprint.register_formatter(Path, str)
    objprint(Struct(), honor_existing=False, depth=5)
    objprint.unregister_formatter(Path)

image

Feature: Add a time limit for `op` and `objprint`

Feature request

We can use op or objprint's option depth to control the time spend in printing.But, if we don't know the suitable depth, or even we don't know or can't know what the object is, a time limit can make the program to not stuck.

If you wish, I can bring you a implementation of this.

Question: Is it possible to make linters happy when using `install()`

install() basically injects op into builtins, but linters like mypy or pyright is not aware of that, so IDEs (vscode with pylance, for example) complains about that: "op" is is not defined Pylance(reportUndefinedVariable)

Besides manually ignoring type checking where op is used (# type: ignore), is it even possible to make mypy or pyright happy? Or should I raise an issue there?

question: how to use install()

I created two python file to play with the install() feature:

temp1.py:

from objprint import op, install

# Now you can use op() in any file
install()

temp2.py:

op("hello")

But run the temp2.py, it throws error : NameError: name 'op' is not defined
What is the correct usage of install() ? Thanks !

weird escaped characters in jupyter notebook's output

I used pyreadstat module to read a simple SAS file. the code goes like this in jupyter notebook

import pyreadstat as pyrs
from objprint import op

sas_path = './test_sas/foo.sas7bdat'
df, meta = pyrs.read_sas7bdat(sas_path, metadataonly=True)
# just wanted to check out what meta contains 
op(meta)

the brief output of current cell seems normal

Output exceeds the [size limit]. Open the full output data[ in a text editor]
<metadata_container 0x1bcf4bfa3e0
  .column_labels = [
    'projectid',
    'project',
...
    'project': '$'
  },
  .variable_value_labels = {}
>
<pyreadstat._readstat_parser.metadata_container at 0x1bcf4bfa3e0>

but after I clicked "in a text editor", the first lines look like this:

�[36m<metadata_container 0x1bcf4bfa3e0�[39m
  �[32m.column_labels�[39m = [
    'projectid',
    'project',

the weird characters actually look like small suqares contains 3 small letters ESC in the text editor.

Change the output order of line number and arg name

For the following example:

from objprint import config, install, objjson, op

config(line_number=True)
config(arg_name=True)

x = 1
op(x)

This is the output right now:

x:
<module> (/path/to/objprint-test.py:7)
1

What I think more intuitive is argument name followed by the value and the file and line number at the top. In this particular case:

<module> (/path/to/objprint-test.py:7)
x:
1

Any thoughts?

type 类创建问题

通过type 构建的 class 通过 op 正确打印,这个feature 能否实现?

from objprint import op

class Action:
    pass

def Category(label):
    return type("MCategory", (Action,), {"label": label,
                                        "__type__": "category"})


class ECategory(Action):
    def __init__(self,label):
        self.label = label
        self.__type__ = 'category'



mm = Category('test')
nn = ECategory('test')
op(mm)
op(nn)

>> <class '__main__.MCategory'>
>> <UCategory 0x1f2da7b2b00
>>   .label = 'test'
>> >

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.