├── tests ├── __init__.py ├── test_objerve.py └── test_color.py ├── objerve ├── __init__.py ├── objerve.py └── color.py ├── assets └── logo │ └── objerve.png ├── tox.ini ├── examples └── example.py ├── .github └── workflows │ └── test.yml ├── LICENSE ├── .pre-commit-config.yaml ├── setup.py ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objerve/__init__.py: -------------------------------------------------------------------------------- 1 | from objerve.objerve import Hook, watch 2 | -------------------------------------------------------------------------------- /assets/logo/objerve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkanonder/objerve/HEAD/assets/logo/objerve.png -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist =py{38,39,py310}, pre-commit 3 | 4 | [testenv] 5 | commands = python -m unittest discover -v 6 | 7 | [testenv:pre-commit] 8 | skip_install = true 9 | deps = pre-commit 10 | commands = pre-commit run --all-files 11 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | from objerve import watch 2 | 3 | 4 | @watch(set={"foo", "qux"}, get={"bar", "foo"}, delete={"baz"}) 5 | class M: 6 | qux = "blue" 7 | 8 | def __init__(self): 9 | self.bar = 55 10 | self.foo = 89 11 | self.baz = 121 12 | 13 | 14 | m = M() 15 | m.bar = 233 16 | 17 | 18 | def abc(): 19 | m.foo += 10 20 | 21 | 22 | m.qux = "red" 23 | 24 | 25 | def get_foo(m): 26 | m.bar 27 | 28 | 29 | abc() 30 | m.foo 31 | del m.baz 32 | get_foo(m) 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | pyv: ["3.8", "3.9", "3.10"] 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: actions/setup-python@v3 16 | with: 17 | python-version: ${{ matrix.pyv }} 18 | 19 | - name: Test 20 | run: | 21 | python -m unittest discover -v 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Furkan Taha ÖNDER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | 2 | repos: 3 | - repo: https://github.com/psf/black 4 | rev: 22.6.0 5 | hooks: 6 | - id: black 7 | args: [--line-length=79] 8 | 9 | - repo: https://github.com/PyCQA/isort 10 | rev: 5.10.1 11 | hooks: 12 | - id: isort 13 | args: ["--profile", "black", "--filter-files"] 14 | 15 | - repo: https://github.com/hakancelikdev/unimport 16 | rev: 0.10.0 17 | hooks: 18 | - id: unimport 19 | args: [--remove, --include-star-import, --ignore-init] 20 | 21 | - repo: https://github.com/PyCQA/docformatter 22 | rev: v1.4 23 | hooks: 24 | - id: docformatter 25 | args: [--in-place] 26 | 27 | - repo: https://github.com/pre-commit/mirrors-prettier 28 | rev: v2.7.1 29 | hooks: 30 | - id: prettier 31 | args: [--prose-wrap=always, --print-width=88] 32 | 33 | - repo: https://github.com/pre-commit/pre-commit-hooks 34 | rev: v4.3.0 35 | hooks: 36 | - id: end-of-file-fixer 37 | files: "\\.(py|.txt|.yaml|.json|.in|.md|.toml|.cfg|.html|.yml)$" 38 | 39 | - repo: https://github.com/asottile/pyupgrade 40 | rev: v2.37.3 41 | hooks: 42 | - id: pyupgrade 43 | args: [--py36-plus] -------------------------------------------------------------------------------- /tests/test_objerve.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from objerve import Hook, watch 4 | 5 | 6 | class TestObjerve(unittest.TestCase): 7 | def setUp(self): 8 | @watch(set={"foo", "bar"}, get={"bar"}, delete={"baz"}) 9 | class M: 10 | qux = "blue" 11 | 12 | def __init__(self): 13 | self.bar = 55 14 | self.foo = 89 15 | self.baz = 121 16 | 17 | self.m = M() 18 | 19 | def test_instance(self): 20 | bar_vars = vars(type(self.m).bar) 21 | self.assertIsInstance(type(self.m).bar, Hook) 22 | self.assertIn("get", bar_vars["hooks"]) 23 | self.assertIn("set", bar_vars["hooks"]) 24 | self.assertEqual("bar", bar_vars["public_name"]) 25 | self.assertEqual("_bar", bar_vars["private_name"]) 26 | 27 | foo_vars = vars(type(self.m).foo) 28 | self.assertIsInstance(type(self.m).foo, Hook) 29 | self.assertIn("set", foo_vars["hooks"]) 30 | self.assertEqual("foo", foo_vars["public_name"]) 31 | self.assertEqual("_foo", foo_vars["private_name"]) 32 | 33 | baz_vars = vars(type(self.m).baz) 34 | self.assertIsInstance(type(self.m).baz, Hook) 35 | self.assertIn("delete", baz_vars["hooks"]) 36 | self.assertEqual("baz", baz_vars["public_name"]) 37 | self.assertEqual("_baz", baz_vars["private_name"]) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import setup 4 | 5 | CURRENT_DIR = Path(__file__).parent 6 | 7 | 8 | def get_long_description(): 9 | readme_md = CURRENT_DIR / "README.md" 10 | with open(readme_md, encoding="utf8") as ld_file: 11 | return ld_file.read() 12 | 13 | 14 | setup( 15 | name="objerve", 16 | version="0.2.0", 17 | long_description=get_long_description(), 18 | long_description_content_type="text/markdown", 19 | description="Tiny observer for the attributes of Python objects.", 20 | keywords=["monitoring", "hook", "observer"], 21 | author="Furkan Onder", 22 | author_email="furkanonder@protonmail.com", 23 | url="https://github.com/furkanonder/objerve", 24 | license="MIT", 25 | python_requires=">=3.0", 26 | packages=["objerve"], 27 | install_requires=[], 28 | extras_require={}, 29 | zip_safe=False, 30 | include_package_data=False, 31 | classifiers=[ 32 | "Development Status :: 2 - Pre-Alpha", 33 | "License :: OSI Approved :: MIT License", 34 | "Intended Audience :: End Users/Desktop", 35 | "Programming Language :: Python :: 3", 36 | "Programming Language :: Python :: 3 :: Only", 37 | "Topic :: Software Development :: Libraries :: Python Modules", 38 | "Topic :: Utilities", 39 | "Operating System :: POSIX :: Linux", 40 | "Operating System :: Unix", 41 | "Operating System :: Microsoft :: Windows", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/test_color.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from objerve.color import ( 5 | BLACK, 6 | BLUE, 7 | CYAN, 8 | GREEN, 9 | MAGENTA, 10 | RED, 11 | RESET, 12 | YELLOW, 13 | init_colors, 14 | set_color, 15 | ) 16 | 17 | 18 | class TestColor(unittest.TestCase): 19 | def test_terminal_color_support(self): 20 | init_colors() 21 | 22 | @patch("objerve.color.USE_COLOR", True) 23 | def test_colors(self): 24 | text = "this is test text" 25 | 26 | colored_text = set_color(BLACK, text) 27 | assert BLACK + text + RESET == colored_text 28 | 29 | colored_text = set_color(RED, text) 30 | assert RED + text + RESET == colored_text 31 | 32 | colored_text = set_color(GREEN, text) 33 | assert GREEN + text + RESET == colored_text 34 | 35 | colored_text = set_color(YELLOW, text) 36 | assert YELLOW + text + RESET == colored_text 37 | 38 | colored_text = set_color(BLUE, text) 39 | assert BLUE + text + RESET == colored_text 40 | 41 | colored_text = set_color(MAGENTA, text) 42 | assert MAGENTA + text + RESET == colored_text 43 | 44 | colored_text = set_color(CYAN, text) 45 | assert CYAN + text + RESET == colored_text 46 | 47 | @patch("objerve.color.USE_COLOR", True) 48 | def test_false_color(self): 49 | text = "this is test text" 50 | colored_text = set_color(YELLOW, text) 51 | assert text != colored_text 52 | -------------------------------------------------------------------------------- /objerve/objerve.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from collections import defaultdict 3 | 4 | from objerve.color import CYAN, GREEN, YELLOW, set_color 5 | 6 | 7 | class Hook: 8 | def __init__(self, name, hooks, trace_limit): 9 | self.public_name = name 10 | self.private_name = f"_{name}" 11 | self.hooks = hooks 12 | self.trace_limit = trace_limit 13 | 14 | def __set__(self, obj, value): 15 | if "set" in self.hooks: 16 | self.print_stack(CYAN, f"Set | {self.public_name} = {value}") 17 | setattr(obj, self.private_name, value) 18 | 19 | def __get__(self, obj, objtype=None): 20 | if obj is None: 21 | return self 22 | else: 23 | value = getattr(obj, self.private_name) 24 | if "get" in self.hooks: 25 | self.print_stack(GREEN, f"Get | {self.public_name} = {value}") 26 | return value 27 | 28 | def __delete__(self, instance): 29 | if "delete" in self.hooks: 30 | self.print_stack(YELLOW, f"Delete | {self.public_name}") 31 | delattr(instance, self.private_name) 32 | 33 | def print_stack(self, color, msg): 34 | summary, *_ = traceback.extract_stack(limit=self.trace_limit) 35 | print( 36 | set_color( 37 | color, f"{msg}\n{' '.join(traceback.format_list([summary]))}" 38 | ) 39 | ) 40 | 41 | 42 | def watch(**kwargs): 43 | attrs = defaultdict(list) 44 | trace_limit = kwargs.pop("trace_limit", 3) 45 | 46 | for hook in kwargs: 47 | for attr in kwargs[hook]: 48 | attrs[attr].append(hook) 49 | 50 | def inner(cls): 51 | for attr in attrs: 52 | setattr(cls, attr, Hook(attr, attrs[attr], trace_limit)) 53 | return cls 54 | 55 | return inner 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | *.scssc 4 | *.map 5 | media 6 | 7 | # Created by https://www.gitignore.io/api/python 8 | 9 | ### Python ### 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Environments 98 | .env 99 | .venv 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .dmypy.json 119 | dmypy.json 120 | 121 | ### Python Patch ### 122 | .venv/ 123 | 124 | ### Python.VirtualEnv Stack ### 125 | # Virtualenv 126 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 127 | [Bb]in 128 | [Ii]nclude 129 | [Ll]ib 130 | [Ll]ib64 131 | [Ll]ocal 132 | [Ss]cripts 133 | pyvenv.cfg 134 | pip-selfcheck.json 135 | 136 | 137 | # End of https://www.gitignore.io/api/python 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
11 | 12 | ## Installation 13 | 14 | _objerve_ can be installed by running `pip install objerve` 15 | 16 | ## Example Usage 17 | 18 | Let's say you have a class like that; 19 | 20 | ```python 21 | class M: 22 | qux = "blue" 23 | 24 | def __init__(self): 25 | self.bar = 55 26 | self.foo = 89 27 | self.baz = 121 28 | ``` 29 | 30 | To watch the changes, you need the add the `@watch()` as a class decorator. Within the 31 | arguments of the `watch` decorator you should pass in lists for the keyword arguments of 32 | the attributes you wish to watch. 33 | 34 | ```python 35 | from objerve import watch 36 | 37 | @watch(set={"foo", "qux"}, get={"bar", "foo"}, delete={"baz"}) 38 | class M: 39 | qux = "blue" 40 | 41 | def __init__(self): 42 | self.bar = 55 43 | self.foo = 89 44 | self.baz = 121 45 | 46 | 47 | m = M() 48 | m.bar = 233 49 | 50 | 51 | def abc(): 52 | m.foo += 10 53 | 54 | 55 | m.qux = "red" 56 | 57 | 58 | def get_foo(m): 59 | m.bar 60 | 61 | 62 | abc() 63 | m.foo 64 | del m.baz 65 | get_foo(m) 66 | ``` 67 | 68 | Output: 69 | 70 | ```sh 71 | Set | foo = 89 72 | File "/home/blue/objerve/examples/example.py", line 9, in __init__ 73 | self.foo = 89 74 | 75 | Set | qux = red 76 | File "/home/blue/objerve/examples/example.py", line 21, in