├── requirements-test.txt ├── .github ├── FUNDING.yml └── workflows │ ├── lint.yaml │ ├── release.yaml │ └── build_test.yaml ├── tests ├── data │ └── watchpoints-0.1.5-py3.8.egg ├── __init__.py ├── test_pandas.py ├── test_multithread.py ├── test_unwatch.py ├── test_watch_print.py ├── test_util.py ├── test_trace_func.py ├── test_watch_element.py └── test_watch.py ├── src └── watchpoints │ ├── __init__.py │ ├── util.py │ ├── ast_monkey.py │ ├── watch_print.py │ ├── watch_element.py │ └── watch.py ├── NOTICE.txt ├── Makefile ├── setup.py ├── .gitignore ├── README.md └── LICENSE /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gaogaotiantian 2 | -------------------------------------------------------------------------------- /tests/data/watchpoints-0.1.5-py3.8.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaogaotiantian/watchpoints/HEAD/tests/data/watchpoints-0.1.5-py3.8.egg -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | -------------------------------------------------------------------------------- /src/watchpoints/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import atexit 6 | from .watch import Watch 7 | 8 | __version__ = "0.2.5" 9 | 10 | 11 | __all__ = [ 12 | "watch", 13 | "unwatch" 14 | ] 15 | 16 | 17 | watch = Watch() 18 | unwatch = watch.unwatch 19 | atexit.register(unwatch) 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | flake8-lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: 3.9 18 | - name: Install flake8 19 | run: pip install flake8 20 | - name: Run flake8 21 | run: flake8 src/ tests/ --exclude tests/data --count --ignore=W503 --max-line-length=127 --statistics 22 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Tian Gao 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | refresh: clean build install lint 2 | 3 | build: 4 | python setup.py build 5 | 6 | install: 7 | python setup.py install 8 | 9 | build_dist: 10 | make clean 11 | python setup.py sdist bdist_wheel 12 | pip install dist/*.whl 13 | make test 14 | 15 | release: 16 | python -m twine upload dist/* 17 | 18 | lint: 19 | flake8 src/ tests/ --exclude tests/data/ --count --max-line-length=127 --ignore=W503 20 | 21 | test: 22 | python -m unittest 23 | 24 | clean: 25 | rm -rf __pycache__ 26 | rm -rf tests/__pycache__ 27 | rm -rf src/watchpoints/__pycache__ 28 | rm -rf build 29 | rm -rf dist 30 | rm -rf watchpoints.egg-info 31 | rm -rf src/watchpoints.egg-info 32 | pip uninstall -y watchpoints 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Python package build and publish 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | deploy-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install twine setuptools wheel 21 | - name: Build source tar 22 | run: | 23 | python setup.py sdist bdist_wheel 24 | - name: Publish wheels to PyPI 25 | continue-on-error: true 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | twine upload dist/*.whl dist/*tar* 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md") as f: 4 | long_description = f.read() 5 | 6 | with open("./src/watchpoints/__init__.py") as f: 7 | for line in f.readlines(): 8 | if line.startswith("__version__"): 9 | # __version__ = "0.9" 10 | delim = '"' if '"' in line else "'" 11 | version = line.split(delim)[1] 12 | break 13 | else: 14 | print("Can't find version! Stop Here!") 15 | exit(1) 16 | 17 | setuptools.setup( 18 | name="watchpoints", 19 | version=version, 20 | author="Tian Gao", 21 | author_email="gaogaotiantian@hotmail.com", 22 | description="watchpoints monitors read and write on variables", 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | url="https://github.com/gaogaotiantian/watchpoints", 26 | packages=setuptools.find_packages("src"), 27 | package_dir={"":"src"}, 28 | classifiers=[ 29 | "Development Status :: 4 - Beta", 30 | "Programming Language :: Python :: 3.6", 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Intended Audience :: Developers", 36 | "License :: OSI Approved :: Apache Software License", 37 | "Operating System :: OS Independent" 38 | ], 39 | python_requires=">=3.6", 40 | install_requires = [ 41 | "objprint>=0.1.3" 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /tests/test_pandas.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import unittest 6 | from watchpoints import unwatch, watch 7 | 8 | try: 9 | import pandas as pd 10 | NO_PANDAS = False 11 | except ImportError: 12 | NO_PANDAS = True 13 | 14 | 15 | class CB: 16 | def __init__(self): 17 | self.counter = 0 18 | 19 | def __call__(self, *args): 20 | self.counter += 1 21 | 22 | 23 | @unittest.skipIf( 24 | NO_PANDAS, reason="You need to install pandas. (pip install pandas)" 25 | ) 26 | class TestPandas(unittest.TestCase): 27 | def test_series(self): 28 | def __comparison_series__(obj1, obj2): 29 | return not obj1.equals(obj2) 30 | 31 | cb = CB() 32 | 33 | ss = pd.Series(data=[1, 2, 3], index=list("abc")) 34 | 35 | watch(ss, cmp=__comparison_series__, callback=cb) 36 | 37 | # Should watch here 38 | self.assertEqual(cb.counter, 0) 39 | 40 | ss.loc["a"] = 10 41 | 42 | self.assertEqual(cb.counter, 1) 43 | 44 | unwatch() 45 | 46 | def test_dataframe_cmp(self): 47 | def __comparison_dataframe__(obj1, obj2): 48 | return not obj1.equals(obj2) 49 | 50 | cb = CB() 51 | 52 | df = pd.DataFrame( 53 | data=[[1, 2], [3, 4], [5, 6]], index=list("abc"), columns=list("AB") 54 | ) 55 | 56 | watch(df, cmp=__comparison_dataframe__, callback=cb) 57 | 58 | # Other stuff happens 59 | a = 2 60 | _ = a + 5 61 | 62 | self.assertEqual(cb.counter, 0) 63 | 64 | df.loc["a", "B"] = 10 65 | 66 | self.assertEqual(cb.counter, 1) 67 | 68 | unwatch() 69 | 70 | def test_dataframe(self): 71 | cb = CB() 72 | 73 | df = pd.DataFrame( 74 | data=[[1, 2], [3, 4], [5, 6]], index=list("abc"), columns=list("AB") 75 | ) 76 | 77 | watch(df, callback=cb) 78 | 79 | # Other stuff happens 80 | a = 2 81 | _ = a + 5 82 | 83 | self.assertEqual(cb.counter, 0) 84 | 85 | df.loc["a", "B"] = 10 86 | 87 | self.assertEqual(cb.counter, 1) 88 | 89 | unwatch() 90 | -------------------------------------------------------------------------------- /tests/test_multithread.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | from watchpoints import watch, unwatch 6 | from watchpoints.watch_print import WatchPrint 7 | from contextlib import redirect_stdout 8 | import io 9 | import inspect 10 | import unittest 11 | import threading 12 | import time 13 | import sys 14 | 15 | 16 | class myThread (threading.Thread): 17 | def __init__(self, threadID, name, obj): 18 | threading.Thread.__init__(self) 19 | self.threadID = threadID 20 | self.name = name 21 | self.obj = obj 22 | 23 | def run(self): 24 | for i in range(self.threadID * 5, (self.threadID + 1) * 5): 25 | self.obj[0] = i 26 | time.sleep(0.001) 27 | 28 | 29 | class CB: 30 | def __init__(self): 31 | self.counter = 0 32 | 33 | def __call__(self, *args): 34 | self.counter += 1 35 | 36 | 37 | class TestMultiThraed(unittest.TestCase): 38 | def test_basic(self): 39 | cb = CB() 40 | a = [0] 41 | watch(a, callback=cb) 42 | # Create new threads 43 | thread1 = myThread(1, "Thread-1", a) 44 | thread2 = myThread(2, "Thread-2", a) 45 | 46 | # Start new Threads 47 | thread1.start() 48 | thread2.start() 49 | 50 | thread1.join() 51 | thread2.join() 52 | 53 | unwatch() 54 | self.assertEqual(cb.counter, 10) 55 | 56 | def test_watch_print(self): 57 | class Elem: 58 | def __init__(self): 59 | self.alias = None 60 | self.default_alias = "a" 61 | self.prev_obj = "" 62 | self.obj = "" 63 | 64 | a = [0] 65 | # Create new threads 66 | thread1 = myThread(1, "Thread-1", a) 67 | thread2 = myThread(2, "Thread-2", a) 68 | 69 | # Start new Threads 70 | thread1.start() 71 | thread2.start() 72 | 73 | s = io.StringIO() 74 | with redirect_stdout(s): 75 | wp = WatchPrint(file=sys.stdout) 76 | wp(inspect.currentframe(), Elem(), ("a", "b", "c")) 77 | self.assertIn("Thread", s.getvalue()) 78 | 79 | thread1.join() 80 | thread2.join() 81 | 82 | unwatch() 83 | -------------------------------------------------------------------------------- /tests/test_unwatch.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import unittest 6 | from watchpoints import watch, unwatch 7 | 8 | 9 | class CB: 10 | def __init__(self): 11 | self.counter = 0 12 | 13 | def __call__(self, *args): 14 | self.counter += 1 15 | 16 | 17 | class TestUnwatch(unittest.TestCase): 18 | def test_basic(self): 19 | cb = CB() 20 | watch.config(callback=cb) 21 | a = [1, 2, 3] 22 | watch(a) 23 | a[1] = 0 24 | self.assertEqual(cb.counter, 1) 25 | unwatch(a) 26 | a[1] = 1 27 | self.assertEqual(cb.counter, 1) 28 | watch(a) 29 | a[1] = 0 30 | self.assertEqual(cb.counter, 2) 31 | unwatch() 32 | a[1] = 2 33 | self.assertEqual(cb.counter, 2) 34 | 35 | def test_noargs(self): 36 | cb = CB() 37 | watch.config(callback=cb) 38 | a = [1, 2, 3] 39 | watch(a) 40 | a[1] = 0 41 | self.assertEqual(cb.counter, 1) 42 | unwatch() 43 | a[1] = 2 44 | self.assertEqual(cb.counter, 1) 45 | 46 | def test_alias(self): 47 | cb = CB() 48 | watch.config(callback=cb) 49 | a = [1, 2, 3] 50 | watch(a, alias="a") 51 | a[1] = 4 52 | self.assertEqual(cb.counter, 1) 53 | unwatch("a") 54 | a[1] = 5 55 | self.assertEqual(cb.counter, 1) 56 | 57 | def test_long(self): 58 | cb = CB() 59 | watch.config(callback=cb) 60 | a = [1, 2, 3] 61 | watch(a) 62 | a[1] = 0 63 | self.assertEqual(cb.counter, 1) 64 | unwatch(a) 65 | a[1] = 1 66 | self.assertEqual(cb.counter, 1) 67 | watch(a) 68 | a[1] = 0 69 | self.assertEqual(cb.counter, 2) 70 | unwatch() 71 | a[1] = 2 72 | self.assertEqual(cb.counter, 2) 73 | watch(a) 74 | a[1] = 0 75 | self.assertEqual(cb.counter, 3) 76 | unwatch() 77 | a[1] = 2 78 | self.assertEqual(cb.counter, 3) 79 | watch(a, alias="a") 80 | a[1] = 4 81 | self.assertEqual(cb.counter, 4) 82 | unwatch("a") 83 | a[1] = 5 84 | self.assertEqual(cb.counter, 4) 85 | -------------------------------------------------------------------------------- /src/watchpoints/util.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import ast 6 | from io import StringIO 7 | import inspect 8 | import re 9 | try: 10 | import readline 11 | except ImportError: 12 | pass 13 | import sys 14 | from tokenize import generate_tokens, NEWLINE, COMMENT, INDENT, NL 15 | 16 | 17 | def getline(frame): 18 | """ 19 | get the current logic line from the frame 20 | """ 21 | lineno = frame.f_lineno 22 | filename = frame.f_code.co_filename 23 | 24 | if filename == "": 25 | try: 26 | his_length = readline.get_current_history_length() 27 | code_string = readline.get_history_item(his_length) 28 | except NameError: 29 | raise Exception("watchpoints does not support REPL on Windows") 30 | elif filename.startswith(" class WatchPrint:") 46 | 47 | line = wp.getsourceline((None, "file/not/exist", 100)) 48 | self.assertEqual(line, "unable to locate the source") 49 | 50 | def test_print_to_file(self): 51 | wp = WatchPrint(file="tmp_test.log") 52 | wp(inspect.currentframe(), Elem(), ("function", "filename", "c")) 53 | with open("tmp_test.log") as f: 54 | data = f.read() 55 | os.remove("tmp_test.log") 56 | self.assertIn("function", data) 57 | self.assertIn("filename", data) 58 | 59 | def test_custom_printer(self): 60 | class MyObject: 61 | def __init__(self): 62 | self.special_arg = "special_arg" 63 | 64 | elem = Elem() 65 | elem.prev_obj = MyObject() 66 | 67 | s = io.StringIO() 68 | with redirect_stdout(s): 69 | wp = WatchPrint(file=sys.stdout, custom_printer=print) 70 | wp(inspect.currentframe(), elem, ("a", "b", "c")) 71 | self.assertNotIn("special_arg", s.getvalue()) 72 | 73 | s = io.StringIO() 74 | with redirect_stdout(s): 75 | wp = WatchPrint(file=sys.stdout) 76 | wp(inspect.currentframe(), elem, ("a", "b", "c")) 77 | self.assertIn("special_arg", s.getvalue()) 78 | -------------------------------------------------------------------------------- /.github/workflows/build_test.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8, 3.9, '3.10'] 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | timeout-minutes: 30 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | if: matrix.os != 'windows-latest' 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install flake8 setuptools wheel twine coverage 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Install dependencies on Windows 33 | if: matrix.os == 'windows-latest' 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install flake8 setuptools wheel twine coverage 37 | if (Test-Path -Path '.\requirements.txt' -PathType Leaf) {pip install -r requirements.txt} 38 | - name: Lint with flake8 39 | run: | 40 | # stop the build if there are Python syntax errors or undefined names 41 | flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics 42 | # exit-zero treats all errors as warnings. 43 | flake8 src tests --exclude tests/data/ --count --exit-zero --statistic --ignore=E501,E122,E126,E127,E128,W503 44 | - name: Build dist and test with unittest 45 | if: matrix.os != 'windows-latest' 46 | run: | 47 | python setup.py sdist bdist_wheel 48 | if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi 49 | pip install dist/*.whl 50 | python -m unittest 51 | - name: Build dist and test with unittest on Windows 52 | if: matrix.os == 'windows-latest' 53 | run: | 54 | python setup.py sdist bdist_wheel 55 | if (Test-Path -Path '.\requirements-test.txt' -PathType Leaf) {pip install -r requirements-test.txt} 56 | pip install (Get-ChildItem dist/*.whl) 57 | python -m unittest 58 | - name: Generate coverage report 59 | run: | 60 | coverage run --source watchpoints --parallel-mode -m unittest 61 | coverage combine 62 | coverage xml -i 63 | env: 64 | COVERAGE_RUN: True 65 | - name: Upload report to Codecov 66 | uses: codecov/codecov-action@v2 67 | with: 68 | file: ./coverage.xml -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import unittest 6 | import inspect 7 | import sys 8 | from watchpoints.util import getline, getargnodes 9 | 10 | 11 | class TestUtil(unittest.TestCase): 12 | def test_getline(self): 13 | def watch(*args): 14 | frame = inspect.currentframe().f_back 15 | return getline(frame) 16 | 17 | a = [] 18 | b = {} 19 | line = watch(a) 20 | self.assertEqual(line, "line = watch ( a )") 21 | line = watch( 22 | a, 23 | b 24 | ) 25 | self.assertEqual(line, "line = watch ( a , b )") 26 | 27 | def test_getline_with_interpreter(self): 28 | 29 | class FakeCode: 30 | def __init__(self): 31 | self.co_filename = "" 32 | 33 | class FakeFrame: 34 | def __init__(self): 35 | self.f_lineno = 1 36 | self.f_code = FakeCode() 37 | 38 | frame = FakeFrame() 39 | if sys.platform == "win32": 40 | with self.assertRaises(Exception): 41 | line = getline(frame) 42 | else: 43 | import readline 44 | readline.add_history("watch(a)") 45 | line = getline(frame) 46 | self.assertEqual(line, "watch(a)") 47 | 48 | def test_getline_with_ipython(self): 49 | class FakeCode: 50 | def __init__(self): 51 | self.co_filename = "" 52 | 53 | class FakeFrame: 54 | def __init__(self): 55 | self.f_lineno = 1 56 | self.f_code = FakeCode() 57 | 58 | frame = FakeFrame() 59 | with unittest.mock.patch.object(inspect, "getsource", return_value="abc") as _: 60 | line = getline(frame) 61 | self.assertEqual(line, "abc") 62 | 63 | def test_getargnodes(self): 64 | def watch(*args): 65 | frame = inspect.currentframe().f_back 66 | return list(getargnodes(frame)) 67 | 68 | a = [0, 1] 69 | b = {} 70 | argnodes = watch(a) 71 | self.assertEqual(len(argnodes), 1) 72 | self.assertEqual(argnodes[0][1], "a") 73 | argnodes = watch( 74 | a, 75 | b 76 | ) 77 | self.assertEqual(len(argnodes), 2) 78 | self.assertEqual(argnodes[0][1], "a") 79 | self.assertEqual(argnodes[1][1], "b") 80 | argnodes = watch( 81 | a[0], # comments 82 | b 83 | ) 84 | self.assertEqual(len(argnodes), 2) 85 | self.assertEqual(argnodes[0][1], "a[0]") 86 | self.assertEqual(argnodes[1][1], "b") 87 | 88 | with self.assertRaises(Exception): 89 | argnodes = [i for i in watch(a)] 90 | -------------------------------------------------------------------------------- /src/watchpoints/ast_monkey.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import ast 6 | import sys 7 | 8 | 9 | def ast_parse_node(node): 10 | """ 11 | :param ast.Node node: an ast node representing an expression of variable 12 | 13 | :return ast.Node: an ast node for: 14 | _watchpoints_obj = var 15 | if : 16 | # watch(a) 17 | _watchpoints_localvar = "a" 18 | elif : 19 | # watch(a[3]) 20 | _watchpoints_parent = a 21 | _watchpoints_subscr = 3 22 | elif : 23 | # watch(a.b) 24 | _watchpoints_parent = a 25 | _watchpoints_attr = "b" 26 | """ 27 | root = ast.Module( 28 | body=[ 29 | ast.Assign( 30 | targets=[ 31 | ast.Name(id="_watchpoints_obj", ctx=ast.Store()) 32 | ], 33 | value=node 34 | ) 35 | ], 36 | type_ignores=[] 37 | ) 38 | 39 | if type(node) is ast.Name: 40 | root.body.append( 41 | ast.Assign( 42 | targets=[ 43 | ast.Name(id="_watchpoints_localvar", ctx=ast.Store()) 44 | ], 45 | value=ast.Constant(value=node.id) 46 | ) 47 | ) 48 | elif type(node) is ast.Subscript: 49 | root.body.append( 50 | ast.Assign( 51 | targets=[ 52 | ast.Name(id="_watchpoints_parent", ctx=ast.Store()) 53 | ], 54 | value=node.value 55 | ) 56 | ) 57 | if sys.version_info.minor <= 8 and type(node.slice) is ast.Index: 58 | value_node = node.slice.value 59 | elif sys.version_info.minor >= 9 and type(node.slice) is not ast.Slice: 60 | value_node = node.slice 61 | else: 62 | raise ValueError("Slice is not supported!") 63 | 64 | root.body.append( 65 | ast.Assign( 66 | targets=[ 67 | ast.Name(id="_watchpoints_subscr", ctx=ast.Store()) 68 | ], 69 | value=value_node 70 | ) 71 | ) 72 | elif type(node) is ast.Attribute: 73 | root.body.append( 74 | ast.Assign( 75 | targets=[ 76 | ast.Name(id="_watchpoints_parent", ctx=ast.Store()) 77 | ], 78 | value=node.value 79 | ) 80 | ) 81 | root.body.append( 82 | ast.Assign( 83 | targets=[ 84 | ast.Name(id="_watchpoints_attr", ctx=ast.Store()) 85 | ], 86 | value=ast.Constant(value=node.attr) 87 | ) 88 | ) 89 | 90 | ast.fix_missing_locations(root) 91 | 92 | return root 93 | -------------------------------------------------------------------------------- /tests/test_trace_func.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | from contextlib import redirect_stdout 6 | import inspect 7 | import io 8 | import sys 9 | import unittest 10 | from unittest.mock import patch 11 | from watchpoints import watch, unwatch 12 | 13 | 14 | class CB: 15 | def __init__(self): 16 | self.counter = 0 17 | 18 | def __call__(self, *args): 19 | self.counter += 1 20 | 21 | 22 | # This is a coverage/unit test for trace func 23 | # Because coverage.py relies on settrace, we can't test trace func with 24 | # coverage in black-box test. We try to simulate the settrace call here 25 | # so we can get some coverage data 26 | class TestTraceFunc(unittest.TestCase): 27 | def test_trace_func(self): 28 | # Trick watch to let it think it's on 29 | cb = CB() 30 | watch.enable = True 31 | watch.tracefunc_stack.append(sys.gettrace()) 32 | a = 0 33 | watch.tracefunc(inspect.currentframe(), "line", None) 34 | b = [] 35 | watch.tracefunc(inspect.currentframe(), "line", None) 36 | watch(a) 37 | watch(b, callback=cb) 38 | b = 1 39 | watch.tracefunc(inspect.currentframe(), "line", None) 40 | self.assertEqual(cb.counter, 1) 41 | s = io.StringIO() 42 | with redirect_stdout(s): 43 | self.file = sys.stdout 44 | a = 1 45 | watch.tracefunc(inspect.currentframe(), "line", None) 46 | self.assertEqual(s.getvalue(), "") 47 | 48 | unwatch(b) 49 | unwatch() 50 | 51 | def test_unwatch(self): 52 | # Trick watch to let it think it's on 53 | watch.enable = True 54 | watch.tracefunc_stack.append(sys.gettrace()) 55 | a = 0 56 | watch.tracefunc(inspect.currentframe(), "line", None) 57 | watch(a) 58 | unwatch(a) 59 | self.assertEqual(watch.tracefunc_stack, []) 60 | 61 | def test_not_exist(self): 62 | class MyObj: 63 | def __init__(self): 64 | self.a = 0 65 | 66 | watch.enable = True 67 | watch.tracefunc_stack.append(sys.gettrace()) 68 | a = {"a": 0} 69 | watch.tracefunc(inspect.currentframe(), "line", None) 70 | watch(a["a"]) 71 | a.pop("a") 72 | watch.tracefunc(inspect.currentframe(), "line", None) 73 | self.assertEqual(watch.watch_list, []) 74 | 75 | watch(a) 76 | del a 77 | watch.tracefunc(inspect.currentframe(), "line", None) 78 | self.assertEqual(watch.watch_list, []) 79 | 80 | obj = MyObj() 81 | watch(obj.a) 82 | delattr(obj, "a") 83 | watch.tracefunc(inspect.currentframe(), "line", None) 84 | self.assertEqual(watch.watch_list, []) 85 | 86 | unwatch() 87 | self.assertEqual(watch.tracefunc_stack, []) 88 | 89 | @patch('builtins.input', return_value="q\n") 90 | @patch('sys.settrace', return_value=None) 91 | def test_pdb(self, mock_input, mock_settrace): 92 | watch.enable = True 93 | watch.config(pdb=True) 94 | watch.tracefunc_stack.append(sys.gettrace()) 95 | a = 0 96 | watch(a) 97 | watch.tracefunc(inspect.currentframe(), "line", None) 98 | a = 1 99 | watch.tracefunc(inspect.currentframe(), "line", None) 100 | watch.tracefunc(inspect.currentframe(), "line", None) 101 | unwatch(a) 102 | watch.restore() 103 | self.assertEqual(watch.tracefunc_stack, []) 104 | -------------------------------------------------------------------------------- /src/watchpoints/watch_print.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import sys 6 | import threading 7 | from objprint import objstr 8 | import os.path 9 | import zipfile 10 | 11 | 12 | class WatchPrint: 13 | def __init__(self, file=sys.stderr, stack_limit=None, custom_printer=None): 14 | self.file = file 15 | self.stack_limit = stack_limit 16 | self.custom_printer = custom_printer 17 | 18 | def __call__(self, frame, elem, exec_info): 19 | p = self.printer 20 | p("====== Watchpoints Triggered ======") 21 | if threading.active_count() > 1: 22 | curr_thread = threading.current_thread() 23 | p(f"---- {curr_thread.name} ----") 24 | p("Call Stack (most recent call last):") 25 | 26 | curr_frame = frame.f_back 27 | frame_counter = 0 28 | trace_back_data = [] 29 | while curr_frame and (self.stack_limit is None or frame_counter < self.stack_limit - 1): 30 | trace_back_data.append(self._frame_string(curr_frame)) 31 | curr_frame = curr_frame.f_back 32 | frame_counter += 1 33 | 34 | for s in trace_back_data[::-1]: 35 | p(s) 36 | 37 | p(self._file_string(exec_info)) 38 | if elem.alias: 39 | p(f"{elem.alias}:") 40 | elif elem.default_alias: 41 | p(f"{elem.default_alias}:") 42 | p(elem.prev_obj) 43 | p("->") 44 | p(elem.obj) 45 | p("") 46 | 47 | def _file_string(self, exec_info): 48 | return f" {exec_info[0]} ({exec_info[1]}:{exec_info[2]}):\n" + \ 49 | self.getsourceline(exec_info) 50 | 51 | def _frame_string(self, frame): 52 | return self._file_string((frame.f_code.co_name, frame.f_code.co_filename, frame.f_lineno)) 53 | 54 | def getsourceline(self, exec_info): 55 | try: 56 | filename = exec_info[1] 57 | if os.path.exists(filename): 58 | with open(exec_info[1], encoding="utf-8") as f: 59 | lines = f.readlines() 60 | return f"> {lines[exec_info[2] - 1].strip()}" 61 | else: 62 | # We may have an egg file, we try to figure out if we have a zipfile 63 | # in the path and unzip that 64 | potential_egg = filename 65 | f_paths = [] 66 | while os.path.dirname(potential_egg) != potential_egg: 67 | potential_egg, f_path = os.path.split(potential_egg) 68 | f_paths.append(f_path) 69 | if zipfile.is_zipfile(potential_egg): 70 | with zipfile.ZipFile(potential_egg) as zf: 71 | with zf.open("/".join(reversed(f_paths))) as f: 72 | lines = f.readlines() 73 | return f"> {lines[exec_info[2] - 1].decode('utf-8').strip()}" 74 | return "unable to locate the source" 75 | except (FileNotFoundError, PermissionError): # pragma: no cover 76 | return "unable to locate the source" 77 | 78 | def printer(self, obj): 79 | 80 | def do_print(obj, stream): 81 | if self.custom_printer is not None: 82 | self.custom_printer(obj) 83 | else: 84 | if type(obj) is str: 85 | print(obj, file=stream) 86 | else: 87 | print(objstr(obj), file=stream) 88 | 89 | if isinstance(self.file, str): 90 | with open(self.file, "a") as f: 91 | do_print(obj, f) 92 | else: 93 | do_print(obj, self.file) 94 | -------------------------------------------------------------------------------- /src/watchpoints/watch_element.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | from .ast_monkey import ast_parse_node 6 | from .watch_print import WatchPrint 7 | import copy 8 | 9 | try: 10 | import pandas as pd 11 | except ImportError: # pragma: no cover 12 | pd = None 13 | 14 | 15 | class WatchElement: 16 | def __init__(self, frame, node, **kwargs): 17 | code = compile(ast_parse_node(node), "", "exec") 18 | f_locals = frame.f_locals 19 | f_globals = frame.f_globals 20 | exec(code, f_globals, f_locals) 21 | self.frame = frame 22 | self.obj = f_locals.pop("_watchpoints_obj") 23 | self.prev_obj = self.obj 24 | self.prev_obj_repr = self.obj.__repr__() 25 | self.localvar = None 26 | self.parent = None 27 | self.subscr = None 28 | self.attr = None 29 | for var in ("_watchpoints_localvar", "_watchpoints_parent", "_watchpoints_subscr", "_watchpoints_attr"): 30 | if var in f_locals: 31 | setattr(self, var.replace("_watchpoints_", ""), f_locals.pop(var)) 32 | self.alias = kwargs.get("alias", None) 33 | self.default_alias = kwargs.get("default_alias", None) 34 | self._callback = kwargs.get("callback", None) 35 | self.exist = True 36 | self.track = kwargs.get("track", ["variable", "object"]) 37 | self.when = kwargs.get("when", None) 38 | self.deepcopy = kwargs.get("deepcopy", False) 39 | self.cmp = kwargs.get("cmp", None) 40 | self.copy = kwargs.get("copy", None) 41 | self.watch_print = kwargs.get("watch_print", WatchPrint()) 42 | self.update() 43 | 44 | @property 45 | def track(self): 46 | return self._track 47 | 48 | @track.setter 49 | def track(self, val): 50 | if type(val) is list: 51 | for elem in val: 52 | if elem not in ("variable", "object"): 53 | raise ValueError("track only takes list with 'variable' or 'object'") 54 | if len(val) == 0: 55 | raise ValueError("You need to track something!") 56 | self._track = val[:] 57 | elif type(val) is str: 58 | if val not in ("variable", "object"): 59 | raise ValueError("track only takes list with 'variable' or 'object'") 60 | self._track = [val] 61 | else: 62 | raise TypeError("track only takes list with 'variable' or 'object'") 63 | 64 | def changed(self, frame): 65 | """ 66 | :return (changed, exist): 67 | """ 68 | if "variable" in self.track: 69 | if frame is self.frame and self.localvar is not None: 70 | if self.localvar in frame.f_locals: 71 | if self.obj_changed(frame.f_locals[self.localvar]): 72 | self.obj = frame.f_locals[self.localvar] 73 | return True, True 74 | else: 75 | return True, False 76 | 77 | if self.parent is not None and self.subscr is not None: 78 | try: 79 | if self.obj_changed(self.parent[self.subscr]): 80 | self.obj = self.parent[self.subscr] 81 | return True, True 82 | except (IndexError, KeyError): 83 | return True, False 84 | elif self.parent is not None and self.attr is not None: 85 | try: 86 | if self.obj_changed(getattr(self.parent, self.attr)): 87 | self.obj = getattr(self.parent, self.attr) 88 | return True, True 89 | except AttributeError: 90 | return True, False 91 | if "object" in self.track: 92 | if not isinstance(self.obj, type(self.prev_obj)): 93 | raise Exception("object type should not change") # pragma: no cover 94 | else: 95 | return self.obj_changed(self.prev_obj), True 96 | 97 | return False, True 98 | 99 | def obj_changed(self, other): 100 | if not isinstance(self.obj, type(other)): 101 | return True 102 | elif pd is not None and isinstance(self.obj, pd.DataFrame): 103 | return not self.obj.equals(other) 104 | elif self.cmp: 105 | return self.cmp(self.obj, other) 106 | elif self.obj.__class__.__module__ == "builtins": 107 | return self.obj != other 108 | else: 109 | guess = self.obj.__eq__(other) 110 | if guess is NotImplemented: 111 | if self.deepcopy: 112 | raise NotImplementedError( 113 | f"It's impossible to compare deepcopied customize objects." 114 | f"You need to define __eq__ method for {self.obj.__class__}") 115 | return self.obj.__dict__ != other.__dict__ 116 | else: 117 | return not guess 118 | 119 | def update(self): 120 | if pd is not None and isinstance(self.obj, pd.DataFrame): 121 | self.prev_obj = self.obj.copy(True) 122 | elif self.copy: 123 | self.prev_obj = self.copy(self.obj) 124 | elif self.deepcopy: 125 | self.prev_obj = copy.deepcopy(self.obj) 126 | else: 127 | self.prev_obj = copy.copy(self.obj) 128 | self.prev_obj_repr = self.obj.__repr__() 129 | 130 | def same(self, other): 131 | if type(other) is str: 132 | return self.alias and self.alias == other 133 | else: 134 | return other is self.obj 135 | 136 | def belong_to(self, lst): 137 | return any((self.same(other) for other in lst)) 138 | -------------------------------------------------------------------------------- /src/watchpoints/watch.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | from bdb import BdbQuit 6 | import inspect 7 | import pdb 8 | import sys 9 | import threading 10 | from .util import getargnodes 11 | from .watch_element import WatchElement 12 | from .watch_print import WatchPrint 13 | 14 | 15 | class Watch: 16 | def __init__(self): 17 | self.watch_list = [] 18 | self.tracefunc_stack = [] 19 | self.enable = False 20 | self.set_lock = threading.Lock() 21 | self.tracefunc_lock = threading.Lock() 22 | self.restore() 23 | 24 | def __call__(self, *args, **kwargs): 25 | with self.set_lock: 26 | frame = inspect.currentframe().f_back 27 | argnodes = getargnodes(frame) 28 | for node, name in argnodes: 29 | self.watch_list.append( 30 | WatchElement( 31 | frame, 32 | node, 33 | alias=kwargs.get("alias", None), 34 | default_alias=name, 35 | callback=kwargs.get("callback", None), 36 | track=kwargs.get("track", ["variable", "object"]), 37 | when=kwargs.get("when", None), 38 | deepcopy=kwargs.get("deepcopy", False), 39 | cmp=kwargs.get("cmp", None), 40 | copy=kwargs.get("copy", None), 41 | watch_print=WatchPrint( 42 | file=kwargs.get("file", self.file), 43 | stack_limit=kwargs.get("stack_limit", self.stack_limit), 44 | custom_printer=kwargs.get("custom_printer", self.custom_printer) 45 | ) 46 | ) 47 | ) 48 | 49 | if not self.enable and self.watch_list: 50 | self.start_trace(frame) 51 | 52 | del frame 53 | 54 | def start_trace(self, frame): 55 | if not self.enable: 56 | self.enable = True 57 | self.tracefunc_stack.append(sys.gettrace()) 58 | self._prev_funcname = frame.f_code.co_name 59 | self._prev_filename = frame.f_code.co_filename 60 | self._prev_lineno = frame.f_lineno 61 | while frame: 62 | frame.f_trace = self.tracefunc 63 | frame = frame.f_back 64 | 65 | sys.settrace(self.tracefunc) 66 | threading.settrace(self.tracefunc) 67 | 68 | def stop_trace(self, frame): 69 | if self.enable: 70 | self.enable = False 71 | tf = self.tracefunc_stack.pop() 72 | while frame: 73 | frame.f_trace = tf 74 | frame = frame.f_back 75 | 76 | sys.settrace(tf) 77 | threading.settrace(tf) 78 | 79 | def unwatch(self, *args): 80 | if self.enable: 81 | frame = inspect.currentframe().f_back 82 | if not args: 83 | self.watch_list = [] 84 | else: 85 | self.watch_list = [elem for elem in self.watch_list if not elem.belong_to(args)] 86 | 87 | if not self.watch_list: 88 | self.stop_trace(frame) 89 | 90 | del frame 91 | 92 | def config(self, **kwargs): 93 | if "callback" in kwargs: 94 | self._callback = kwargs["callback"] 95 | 96 | if "pdb" in kwargs: 97 | self.pdb = pdb.Pdb() 98 | self.pdb.reset() 99 | 100 | if "file" in kwargs: 101 | self.file = kwargs["file"] 102 | 103 | if "stack_limit" in kwargs: 104 | self.stack_limit = kwargs["stack_limit"] 105 | 106 | if "custom_printer" in kwargs: 107 | self.custom_printer = kwargs["custom_printer"] 108 | 109 | def restore(self): 110 | self._callback = self._default_callback 111 | self.pdb = None 112 | self.file = sys.stderr 113 | self.pdb_enable = False 114 | self.stack_limit = 5 115 | self.custom_printer = None 116 | 117 | def install(self, func="watch"): 118 | import builtins 119 | setattr(builtins, func, self) 120 | 121 | def uninstall(self, func="watch"): 122 | import builtins 123 | if hasattr(builtins, func): 124 | delattr(builtins, func) 125 | 126 | def tracefunc(self, frame, event, arg): 127 | with self.tracefunc_lock: 128 | dirty = False 129 | for elem in self.watch_list: 130 | changed, exist = elem.changed(frame) 131 | if changed: 132 | if not elem.when or elem.when(elem.obj): 133 | if self.pdb: 134 | self.pdb_enable = True 135 | if elem._callback: 136 | elem._callback(frame, elem, (self._prev_funcname, self._prev_filename, self._prev_lineno)) 137 | else: 138 | self._callback(frame, elem, (self._prev_funcname, self._prev_filename, self._prev_lineno)) 139 | elem.update() 140 | if not exist: 141 | elem.exist = False 142 | dirty = True 143 | if dirty: 144 | self.watch_list = [elem for elem in self.watch_list if elem.exist] 145 | 146 | self._prev_funcname = frame.f_code.co_name 147 | self._prev_filename = frame.f_code.co_filename 148 | self._prev_lineno = frame.f_lineno 149 | 150 | if self.pdb_enable: 151 | try: 152 | self.pdb.trace_dispatch(frame, event, arg) 153 | except BdbQuit: 154 | self.pdb_enable = False 155 | self.pdb.reset() 156 | # BdbQuit will clear sys.settrace() 157 | # We need to get it back 158 | sys.settrace(self.tracefunc) 159 | 160 | return self.tracefunc 161 | 162 | def _default_callback(self, frame, elem, exec_info): 163 | elem.watch_print(frame, elem, exec_info) 164 | -------------------------------------------------------------------------------- /tests/test_watch_element.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | import unittest 6 | import inspect 7 | import os 8 | import pandas as pd 9 | from watchpoints.watch_element import WatchElement 10 | from watchpoints.util import getargnodes 11 | 12 | 13 | class TestWatchElement(unittest.TestCase): 14 | def helper(self, *args, **kwargs): 15 | frame = inspect.currentframe().f_back 16 | argnodes = getargnodes(frame) 17 | return [ 18 | WatchElement( 19 | frame, 20 | node, 21 | default_alias=name, 22 | **kwargs 23 | ) for node, name in argnodes 24 | ] 25 | 26 | def test_basic(self): 27 | a = 0 28 | b = [] 29 | c = {} 30 | d = set() 31 | 32 | lst = self.helper(a, b, 33 | c, d) 34 | self.assertEqual(len(lst), 4) 35 | 36 | def test_changed(self): 37 | class MyObj: 38 | def __init__(self): 39 | self.a = 0 40 | 41 | a = [] 42 | b = [1, 2] 43 | c = MyObj() 44 | frame = inspect.currentframe() 45 | lst = self.helper(a, b[0], c.a) 46 | wea = lst[0] 47 | web = lst[1] 48 | wec = lst[2] 49 | c.a = 0 50 | self.assertFalse(wec.changed(frame)[0]) 51 | a.append(1) 52 | b[0] = 3 53 | c.a = 5 54 | self.assertTrue(wea.changed(frame)[0]) 55 | self.assertTrue(web.changed(frame)[0]) 56 | self.assertTrue(wec.changed(frame)[0]) 57 | lst = self.helper(c) 58 | wec = lst[0] 59 | c.a = 5 60 | self.assertFalse(wec.changed(frame)[0]) 61 | c.a = 6 62 | self.assertTrue(wec.changed(frame)[0]) 63 | 64 | def test_same(self): 65 | a = [] 66 | b = a 67 | lst = self.helper(a, alias="a") 68 | self.assertTrue(lst[0].same(a)) 69 | self.assertTrue(lst[0].same(b)) 70 | self.assertTrue(lst[0].same("a")) 71 | self.assertTrue(lst[0].belong_to([a])) 72 | self.assertFalse(lst[0].belong_to(["b"])) 73 | 74 | def test_track(self): 75 | a = [] 76 | b = [1, 2] 77 | frame = inspect.currentframe() 78 | lst = self.helper(a, track="variable") 79 | wea = lst[0] 80 | lst = self.helper(b, track="object") 81 | web = lst[0] 82 | a.append(1) 83 | self.assertFalse(wea.changed(frame)[0]) 84 | a = {} 85 | self.assertTrue(wea.changed(frame)[0]) 86 | b[0] = 3 87 | self.assertTrue(web.changed(frame)[0]) 88 | web.update() 89 | b = {} 90 | self.assertFalse(web.changed(frame)[0]) 91 | 92 | def test_when(self): 93 | a = [0] 94 | lst = self.helper(a, when=lambda x: x[0] > 0) 95 | wea = lst[0] 96 | self.assertFalse(wea.when(wea.obj)) 97 | a[0] = 1 98 | self.assertTrue(wea.when(wea.obj)) 99 | 100 | def test_deepcopy(self): 101 | frame = inspect.currentframe() 102 | a = {"a": [1, 2]} 103 | lst = self.helper(a) 104 | wea = lst[0] 105 | a["a"][0] = 3 106 | self.assertFalse(wea.changed(frame)[0]) 107 | a = {"a": [1, 2]} 108 | lst = self.helper(a, deepcopy=True) 109 | wea = lst[0] 110 | a["a"][0] = 3 111 | self.assertTrue(wea.changed(frame)[0]) 112 | 113 | def test_custom_cmp(self): 114 | 115 | def cmp(obj1, obj2): 116 | return False 117 | 118 | frame = inspect.currentframe() 119 | a = {"a": 1} 120 | lst = self.helper(a, cmp=cmp) 121 | wea = lst[0] 122 | a["a"] = 0 123 | self.assertFalse(wea.changed(frame)[0]) 124 | 125 | def test_custom_copy(self): 126 | 127 | def copy(obj1): 128 | return {"a": 0} 129 | 130 | frame = inspect.currentframe() 131 | a = {"a": 1} 132 | lst = self.helper(a, copy=copy) 133 | wea = lst[0] 134 | a["a"] = 0 135 | self.assertFalse(wea.changed(frame)[0]) 136 | 137 | a["a"] = 1 138 | self.assertTrue(wea.changed(frame)[0]) 139 | 140 | def test_object(self): 141 | class MyObj: 142 | def __init__(self): 143 | self.a = {"a": 1} 144 | 145 | class MyObjWithEq: 146 | def __init__(self): 147 | self.a = {"a": 1} 148 | 149 | def __eq__(self, other): 150 | return self.a == other.a 151 | 152 | obj = MyObj() 153 | obj_eq = MyObjWithEq() 154 | frame = inspect.currentframe() 155 | 156 | lst = self.helper(obj, obj_eq) 157 | wobj = lst[0] 158 | wobj_eq = lst[1] 159 | obj.a["a"] = 2 160 | self.assertFalse(wobj.changed(frame)[0]) 161 | obj_eq.a["a"] = 2 162 | self.assertFalse(wobj_eq.changed(frame)[0]) 163 | 164 | lst = self.helper(obj, obj_eq, deepcopy=True) 165 | wobj = lst[0] 166 | wobj_eq = lst[1] 167 | obj.a["a"] = 3 168 | with self.assertRaises(NotImplementedError): 169 | wobj.changed(frame)[0] 170 | obj_eq.a["a"] = 3 171 | self.assertTrue(wobj_eq.changed(frame)[0]) 172 | 173 | def test_dataframe(self): 174 | frame = inspect.currentframe() 175 | df = pd.DataFrame([1, 2, 3]) 176 | lst = self.helper(df) 177 | wedf = lst[0] 178 | self.assertFalse(wedf.changed(frame)[0]) 179 | df.iloc[0] = 0 180 | self.assertTrue(wedf.changed(frame)[0]) 181 | 182 | def test_global_module(self): 183 | os.environ['a'] = 'test' 184 | self.helper(os.environ['a']) 185 | 186 | def test_invalid(self): 187 | a = [1, 2, 3] 188 | with self.assertRaises(ValueError): 189 | self.helper(a[0:2]) 190 | 191 | with self.assertRaises(ValueError): 192 | self.helper(a, track=[]) 193 | 194 | with self.assertRaises(ValueError): 195 | self.helper(a, track=["invalid"]) 196 | 197 | with self.assertRaises(ValueError): 198 | self.helper(a, track="invalid") 199 | 200 | with self.assertRaises(TypeError): 201 | self.helper(a, track=123) 202 | -------------------------------------------------------------------------------- /tests/test_watch.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | # For details: https://github.com/gaogaotiantian/watchpoints/blob/master/NOTICE.txt 3 | 4 | 5 | from contextlib import redirect_stdout 6 | import io 7 | import os 8 | import sys 9 | import unittest 10 | from watchpoints import watch, unwatch 11 | 12 | 13 | class CB: 14 | def __init__(self): 15 | self.counter = 0 16 | 17 | def __call__(self, *args): 18 | self.counter += 1 19 | 20 | 21 | class TestWatch(unittest.TestCase): 22 | def setUp(self): 23 | unwatch() 24 | watch.restore() 25 | 26 | def tearDown(self): 27 | watch.restore() 28 | return super().tearDown() 29 | 30 | def test_basic(self): 31 | cb = CB() 32 | watch.config(callback=cb) 33 | a = [1, 2, 3] 34 | watch(a) 35 | a[0] = 2 36 | a.append(4) 37 | b = a 38 | b.append(5) 39 | a = {"a": 1} 40 | a["b"] = 2 41 | 42 | def change(d): 43 | d["c"] = 3 44 | 45 | change(a) 46 | 47 | self.assertEqual(cb.counter, 6) 48 | unwatch() 49 | 50 | def test_subscr(self): 51 | cb = CB() 52 | watch.config(callback=cb) 53 | a = [1, 2, 3] 54 | watch(a[1]) 55 | a[0] = 2 56 | a[1] = 3 57 | self.assertEqual(cb.counter, 1) 58 | 59 | with self.assertRaises(ValueError): 60 | watch(a[0:2]) 61 | 62 | def val(arg): 63 | return 1 64 | 65 | a[val(3)] = 4 66 | self.assertEqual(cb.counter, 2) 67 | unwatch() 68 | 69 | a = {"a": 1} 70 | watch(a["a"]) 71 | a["a"] = 2 72 | a["b"] = 3 73 | self.assertEqual(cb.counter, 3) 74 | 75 | unwatch() 76 | 77 | def test_attr(self): 78 | class MyObj: 79 | def __init__(self): 80 | self.a = 0 81 | 82 | cb = CB() 83 | watch.config(callback=cb) 84 | obj = MyObj() 85 | watch(obj.a) 86 | obj.a = 1 87 | self.assertEqual(cb.counter, 1) 88 | unwatch(obj.a) 89 | obj.a = 2 90 | self.assertEqual(cb.counter, 1) 91 | watch(obj) 92 | obj.a = 3 93 | self.assertEqual(cb.counter, 2) 94 | obj.a = 3 95 | self.assertEqual(cb.counter, 2) 96 | 97 | def test_element_callback(self): 98 | cb = CB() 99 | a = [1, 2, 3] 100 | watch(a, callback=cb) 101 | a[0] = 2 102 | a.append(4) 103 | b = a 104 | b.append(5) 105 | a = {"a": 1} 106 | a["b"] = 2 107 | 108 | def change(d): 109 | d["c"] = 3 110 | 111 | change(a) 112 | 113 | self.assertEqual(cb.counter, 6) 114 | unwatch() 115 | 116 | def test_track(self): 117 | cb = CB() 118 | a = [1, 2, 3] 119 | b = [1, 2, 3] 120 | watch(a, callback=cb, track="object") 121 | a[0] = 2 122 | self.assertEqual(cb.counter, 1) 123 | a = {} 124 | self.assertEqual(cb.counter, 1) 125 | watch(b, callback=cb, track=["variable"]) 126 | b[0] = 2 127 | self.assertEqual(cb.counter, 1) 128 | b = {} 129 | self.assertEqual(cb.counter, 2) 130 | 131 | with self.assertRaises(ValueError): 132 | c = [] 133 | watch(c, track=["invalid"]) 134 | 135 | with self.assertRaises(ValueError): 136 | c = [] 137 | watch(c, track="invalid") 138 | 139 | with self.assertRaises(ValueError): 140 | c = [] 141 | watch(c, track=[]) 142 | 143 | with self.assertRaises(TypeError): 144 | c = [] 145 | watch(c, track={}) 146 | 147 | unwatch() 148 | 149 | def test_when(self): 150 | cb = CB() 151 | a = 0 152 | watch(a, callback=cb, when=lambda x: x > 0) 153 | a = -1 154 | a = 1 155 | a = 2 156 | a = -3 157 | self.assertEqual(cb.counter, 2) 158 | unwatch() 159 | 160 | def test_deepcopy(self): 161 | cb = CB() 162 | a = {"a": [0]} 163 | watch(a, callback=cb) 164 | a["a"][0] = 1 165 | self.assertEqual(cb.counter, 0) 166 | unwatch() 167 | watch(a, callback=cb, deepcopy=True) 168 | a["a"][0] = 2 169 | self.assertEqual(cb.counter, 1) 170 | unwatch() 171 | 172 | def test_custom_copy(self): 173 | 174 | def copy(obj): 175 | return {"a": 0} 176 | 177 | cb = CB() 178 | a = {"a": 0} 179 | watch(a, callback=cb, copy=copy) 180 | a["a"] = 1 181 | self.assertEqual(cb.counter, 1) 182 | a["a"] = 1 183 | self.assertGreater(cb.counter, 2) 184 | unwatch() 185 | 186 | def test_custom_cmp(self): 187 | 188 | def cmp(obj1, obj2): 189 | return False 190 | 191 | cb = CB() 192 | a = {"a": 0} 193 | watch(a, callback=cb, cmp=cmp) 194 | a["a"] = 1 195 | self.assertEqual(cb.counter, 0) 196 | a["a"] = 2 197 | self.assertEqual(cb.counter, 0) 198 | unwatch() 199 | 200 | def test_install(self): 201 | watch.install("_watch") 202 | _watch() # noqa 203 | cb = CB() 204 | a = [1, 2, 3] 205 | watch(a, callback=cb) 206 | a[0] = 2 207 | self.assertEqual(cb.counter, 1) 208 | _watch.unwatch() # noqa 209 | watch.uninstall("_watch") 210 | with self.assertRaises(NameError): 211 | _watch(a) # noqa 212 | 213 | def test_printer(self): 214 | s = io.StringIO() 215 | with redirect_stdout(s): 216 | watch.config(file=sys.stdout) 217 | a = [1, 2, 3] 218 | watch(a) 219 | a[0] = 2 220 | unwatch() 221 | self.assertNotEqual(s.getvalue(), "") 222 | 223 | def test_custom_printer(self): 224 | s = io.StringIO() 225 | with redirect_stdout(s): 226 | watch.config(file=sys.stdout) 227 | a = [i for i in range(100)] 228 | watch(a, custom_printer=print) 229 | a = [i + 1 for i in range(100)] 230 | unwatch() 231 | self.assertLess(s.getvalue().count("\n"), 100) 232 | 233 | with redirect_stdout(s): 234 | watch.config(file=sys.stdout, custom_printer=print) 235 | a = [i for i in range(100)] 236 | watch(a) 237 | a = [i + 1 for i in range(100)] 238 | unwatch() 239 | self.assertLess(s.getvalue().count("\n"), 100) 240 | 241 | def test_stack_limit_global(self): 242 | watch.config(stack_limit=1) 243 | s = io.StringIO() 244 | with redirect_stdout(s): 245 | watch.config(file=sys.stdout) 246 | a = [1, 2, 3] 247 | watch(a) 248 | a[0] = 2 249 | unwatch() 250 | self.assertEqual(s.getvalue().count("> "), 1) 251 | 252 | def test_stack_limit_local(self): 253 | s = io.StringIO() 254 | with redirect_stdout(s): 255 | a = [1, 2, 3] 256 | watch(a, file=sys.stdout, stack_limit=1) 257 | a[0] = 2 258 | unwatch() 259 | self.assertEqual(s.getvalue().count("> "), 1) 260 | 261 | def test_write_to_file_stream(self): 262 | f = open("tmp_test.log", "w") 263 | a = [1, 2, 3] 264 | watch(a, file=f, stack_limit=1) 265 | a[0] = 2 266 | unwatch() 267 | f.close() 268 | with open("tmp_test.log") as f: 269 | data = f.read() 270 | os.remove("tmp_test.log") 271 | self.assertEqual(data.count("> "), 1) 272 | 273 | def test_write_to_file(self): 274 | a = [1, 2, 3] 275 | watch(a, file="tmp_test.log") 276 | a[0] = 2 277 | unwatch() 278 | with open("tmp_test.log") as f: 279 | data = f.read() 280 | os.remove("tmp_test.log") 281 | self.assertIn("a[0] = 2", data) 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watchpoints 2 | 3 | [![build](https://github.com/gaogaotiantian/watchpoints/workflows/build/badge.svg)](https://github.com/gaogaotiantian/watchpoints/actions?query=workflow%3Abuild) [![coverage](https://img.shields.io/codecov/c/github/gaogaotiantian/watchpoints)](https://codecov.io/gh/gaogaotiantian/watchpoints) [![pypi](https://img.shields.io/pypi/v/watchpoints.svg)](https://pypi.org/project/watchpoints/) [![support-version](https://img.shields.io/pypi/pyversions/watchpoints)](https://img.shields.io/pypi/pyversions/watchpoints) [![license](https://img.shields.io/github/license/gaogaotiantian/watchpoints)](https://github.com/gaogaotiantian/watchpoints/blob/master/LICENSE) [![commit](https://img.shields.io/github/last-commit/gaogaotiantian/watchpoints)](https://github.com/gaogaotiantian/watchpoints/commits/master) 4 | 5 | watchpoints is an easy-to-use, intuitive variable/object monitor tool for python that behaves similar to watchpoints in gdb. 6 | 7 | ## Install 8 | 9 | ``` 10 | pip install watchpoints 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### watch 16 | 17 | Simply ```watch``` the variables you need to monitor! 18 | 19 | ```python 20 | from watchpoints import watch 21 | 22 | a = 0 23 | watch(a) 24 | a = 1 25 | ``` 26 | 27 | will generate 28 | 29 | ``` 30 | ====== Watchpoints Triggered ====== 31 | Call Stack (most recent call last): 32 | (my_script.py:5): 33 | > a = 1 34 | a: 35 | 0 36 | -> 37 | 1 38 | ``` 39 | 40 | It works on both variable change and object change 41 | 42 | ```python 43 | from watchpoints import watch 44 | 45 | a = [] 46 | watch(a) 47 | a.append(1) # Trigger 48 | a = {} # Trigger 49 | ``` 50 | 51 | Even better, it can track the changes of the object after the changes of the variable 52 | 53 | ```python 54 | from watchpoints import watch 55 | 56 | a = [] 57 | watch(a) 58 | a = {} # Trigger 59 | a["a"] = 2 # Trigger 60 | ``` 61 | 62 | Without doubts, it works whenever the object is changed, even if it's not in the same scope 63 | 64 | ```python 65 | from watchpoints import watch 66 | 67 | def func(var): 68 | var["a"] = 1 69 | 70 | a = {} 71 | watch(a) 72 | func(a) 73 | ``` 74 | 75 | ``` 76 | ====== Watchpoints Triggered ====== 77 | Call Stack (most recent call last): 78 | (my_script.py:8): 79 | > func(a) 80 | func (my_script.py:4): 81 | > var["a"] = 1 82 | a: 83 | {} 84 | -> 85 | {'a': 1} 86 | ``` 87 | 88 | As you can imagine, you can monitor attributes of an object, or a specific element of a list or a dict 89 | 90 | ```python 91 | from watchpoints import watch 92 | 93 | class MyObj: 94 | def __init__(self): 95 | self.a = 0 96 | 97 | obj = MyObj() 98 | d = {"a": 0} 99 | watch(obj.a, d["a"]) # Yes you can do this 100 | obj.a = 1 # Trigger 101 | d["a"] = 1 # Trigger 102 | ``` 103 | 104 | Also, watchpoints supports native ```threading``` library for multi-threading. It will tell you which thread is changing the 105 | value as well. 106 | 107 | ``` 108 | ====== Watchpoints Triggered ====== 109 | ---- Thread-1 ---- 110 | Call Stack (most recent call last): 111 | _bootstrap (/usr/lib/python3.8/threading.py:890): 112 | > self._bootstrap_inner() 113 | _bootstrap_inner (/usr/lib/python3.8/threading.py:932): 114 | > self.run() 115 | run (my_script.py:15): 116 | > a[0] = i 117 | a: 118 | [0] 119 | -> 120 | [1] 121 | ``` 122 | 123 | **watchpoints will try to guess what you want to monitor, and monitor it as you expect**(well most of the time) 124 | 125 | ### unwatch 126 | 127 | When you are done with the variable, you can unwatch it. 128 | 129 | ```python 130 | from watchpoints import watch, unwatch 131 | 132 | a = 0 133 | watch(a) 134 | a = 1 135 | unwatch(a) 136 | a = 2 # nothing will happen 137 | ``` 138 | 139 | Or you can unwatch everything by passing no argument to it 140 | 141 | ```python 142 | unwatch() # unwatch everything 143 | ``` 144 | 145 | ### print to different stream 146 | 147 | Like the ``print`` function, you can choose the output stream for watch print using ``file`` argument. The default 148 | value is ``sys.stderr``. 149 | 150 | ```python 151 | f = open("watch.log", "w") 152 | a = 0 153 | watch(a, file=f) 154 | a = 1 155 | f.close() 156 | ``` 157 | 158 | Be aware that **the stream needs to be available when the variable is changed**! So the following code **WON'T WORK**: 159 | 160 | ```python 161 | a = 0 162 | with open("watch.log", "w") as f: 163 | watch(a, file=f) 164 | a = 1 165 | ``` 166 | 167 | Or you could just give a filename to ``watch``. It will append to the file. 168 | 169 | ```python 170 | watch(a, file="watch.log") 171 | ``` 172 | 173 | Use config if you want to make it global 174 | 175 | ```python 176 | watch.config(file="watch.log") 177 | ``` 178 | 179 | ### customize printer 180 | 181 | You can use your own printer function to print the object, instead of the default ``objprint`` with ``custom_printer`` 182 | 183 | ```python 184 | # This will use built-in print function for the objects 185 | watch(a, custom_printer=print) 186 | ``` 187 | 188 | Use config if you want to make it global 189 | 190 | ```python 191 | watch.config(custom_printer=print) 192 | ``` 193 | 194 | ### alias 195 | 196 | You can give an alias to a monitored variable, so you can unwatch it anywhere. And the alias will be printed instead of the variable name 197 | ```python 198 | from watchpoints import watch, unwatch 199 | 200 | watch(a, alias="james") 201 | # Many other stuff, scope changes 202 | unwatch("james") 203 | ``` 204 | 205 | ### conditional callback 206 | 207 | You can give an extra condition filter to do "conditional watchpoints". Pass a function ```func(obj)``` which returns ```True``` 208 | if you want to trigger the callback to ```when``` of ```watch``` 209 | 210 | ```python 211 | a = 0 212 | watch(a, when=lambda x: x > 0) 213 | a = -1 # Won't trigger 214 | a = 1 # Trigger 215 | ``` 216 | 217 | ### variable vs object 218 | 219 | When you do ```watch()``` on an object, you are actually tracking both the object and the variable holding it. In most cases, that's what 220 | you want anyways. However, you can configure precisely which you want to track. 221 | 222 | ```python 223 | a = [] 224 | watch(a, track="object") 225 | a.append(1) # Trigger 226 | a = {} # Won't trigger because the list object does not change 227 | 228 | a = [] 229 | watch(a, track="variable") 230 | a.append(1) # Won't trigger, because "a" still holds the same object 231 | a = {} # Trigger 232 | ``` 233 | 234 | ### object compare and deepcopy 235 | 236 | Nested object comparison is tricky. It's hard to find a solid standard to compare complicated customized objects. 237 | By default, watchpoints will do a shallow copy of the object. You can override this behavior by passing ```deepcopy=True``` to ```watch()``` 238 | 239 | ```python 240 | watch(a, deepcopy=True) 241 | ``` 242 | 243 | watchpoints will honor ```__eq__``` method for user-defined classes first. If ```__eq__``` is not implemented, watchpoints will compare 244 | ```__dict__```(basically attibures) of the object if using shallow copy, and raise an ```NotImplementedError``` if using deepcopy. 245 | 246 | The reason behind this is, if you deepcopied a complicated structure, there's no way for watchpoints to figure out if it's the same object 247 | without user defined ```__eq__``` function. 248 | 249 | #### customize copy and compare 250 | 251 | For your own data structures, you can provide a customized copy and/or customized compare function for watchpoints to better suit your need. 252 | 253 | watchpoints will use the copy function you provide to copy the object for reference, and use your compare function to check if that 254 | object is changed. If copy function or compare function is not provided, it falls to default as mentioned above. 255 | 256 | ```cmp``` argument takes a function that will take two objects as arguments and return a ```boolean``` representing whether the objects 257 | are **different** 258 | 259 | ```python 260 | def my_cmp(obj1, obj2): 261 | return obj1.id != obj2.id 262 | 263 | watch(a, cmp=my_cmp) 264 | ``` 265 | 266 | ```copy``` argument takes a function that will take a object and return a copy of it 267 | 268 | ```python 269 | def my_copy(obj): 270 | return MyObj(id=obj.id) 271 | 272 | watch(a, copy=my_copy) 273 | ``` 274 | 275 | ### stack limit 276 | 277 | You can specify the call stack limit printed using ```watch.config()```. The default value is ```5```, any positive integer is accepted. 278 | You can use ```None``` for unlimited call stack, which means it will prints out all the frames. 279 | 280 | ```python 281 | watch.config(stack_limit=10) 282 | ``` 283 | 284 | You can also set different stack limits for each monitored variable by passing ``stack_limit`` argument to ``watch`` 285 | 286 | ```python 287 | # This will only change stack_limit for a 288 | watch(a, stack_limit=10) 289 | ``` 290 | 291 | ### customize callback 292 | 293 | Of course sometimes you want to print in your own format, or even do something more than print. You can use your own callback for monitored variables 294 | 295 | ```python 296 | watch(a, callback=my_callback) 297 | ``` 298 | 299 | The callback function takes three arguments 300 | 301 | ```python 302 | def my_callback(frame, elem, exec_info) 303 | ``` 304 | 305 | * ```frame``` is the current frame when a change is detected. 306 | * ```elem``` is a ```WatchElement``` object that I'm too lazy to describe for now. 307 | * ```exec_info``` is a tuple of ```(funcname, filename, lineno)``` of the line that changed the variable 308 | 309 | You can also set change the callback function globally by 310 | 311 | ```python 312 | watch.config(callback=my_callback) 313 | ``` 314 | 315 | Use ```restore()``` to restore the default callback 316 | ```python 317 | watch.restore() 318 | ``` 319 | 320 | ### Integrating with pdb 321 | 322 | watchpoints can be used with pdb with ease. You can trigger pdb just like using ```breakpoint()``` when 323 | your monitored variable is changed. Simply do 324 | 325 | ```python 326 | watch.config(pdb=True) 327 | ``` 328 | 329 | When you are in pdb, use ```q(uit)``` command to exit pdb, and the next change on the variable will trigger the pdb again. 330 | 331 | ### Avoid import 332 | 333 | Sometimes it's a hassle having to import the function in every single file. You can install the watch function to builtins 334 | and be able to call it in any files: 335 | 336 | ```python 337 | watch.install() # or watch.install("func_name") and use it as func_name() 338 | # Remove it from the builtins 339 | watch.uninstall() # if installed with a name, pass it to uninstall() as well 340 | ``` 341 | 342 | ## Limitations 343 | 344 | * watchpoints uses ```sys.settrace()``` so it is not compatible with other libraries that use the same function. 345 | * watchpoints will slow down your program significantly, like other debuggers, so use it for debugging purpose only 346 | * ```watch()``` needs to be used by itself, not nested in other functions, to be correctly parsed 347 | * at this point, there might be other issues because it's still in development phase 348 | 349 | ## Bugs/Requests 350 | 351 | Please send bug reports and feature requests through [github issue tracker](https://github.com/gaogaotiantian/watchpoints/issues). 352 | 353 | ## License 354 | 355 | Copyright Tian Gao, 2020. 356 | 357 | Distributed under the terms of the [Apache 2.0 license](https://github.com/gaogaotiantian/watchpoints/blob/master/LICENSE). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------