├── peek ├── __init__.py ├── LICENSE.txt └── peek.py ├── .gitignore ├── pyproject.toml ├── tools ├── peek_embedder.py └── install peek.py ├── changelog.md ├── tests └── test_peek.py └── readme.md /peek/__init__.py: -------------------------------------------------------------------------------- 1 | from .peek import * 2 | from .peek import __version__ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.pyc 3 | __pycache__/ 4 | *.egg-info/ 5 | .venv/ 6 | dist/* 7 | build/* 8 | build_and_upload.bat 9 | toml_versioner.py 10 | toml_update_requirements.py 11 | upload_html.py 12 | *.html 13 | misc/* 14 | artwork/* 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | ] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "peek-python" 9 | authors = [ 10 | { name = "Ruud van der Ham", email = "rt.van.der.ham@gmail.com" }, 11 | ] 12 | description = "peek - like print, but easy" 13 | version = "25.0.27.post0" 14 | readme = "README.md" 15 | requires-python = ">=3.9" 16 | dependencies = [ 17 | "asttokens>=3.0.0", 18 | "colorama>=0.4.6", 19 | "executing>=2.2.0", 20 | "six>=1.17.0", 21 | "tomli>=2.2.1", 22 | ] 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Programming Language :: Python :: 3 :: Only", 26 | ] 27 | 28 | [project.urls] 29 | Homepage = "https://github.com/salabim/peek" 30 | Repository = "https://github.com/salabim/peek" 31 | 32 | [tool.setuptools] 33 | packages = [ 34 | "peek", 35 | ] 36 | 37 | [tool.setuptools.package-data] 38 | "*" = [ 39 | "*.json", 40 | ] 41 | -------------------------------------------------------------------------------- /peek/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2024 Ruud van der Ham, ruud@salabim.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tools/peek_embedder.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import zlib 3 | from pathlib import Path 4 | import sys 5 | import os 6 | 7 | 8 | def embed_package(infile, package, prefer_installed=False, py_files_only=True, outfile=None): 9 | """ 10 | build outfile from infile with package(s) as mentioned in package embedded 11 | 12 | Arguments 13 | --------- 14 | infile : str or pathlib.Path 15 | input file 16 | 17 | package : str or tuple/list of str 18 | package(s) to be embedded 19 | 20 | prefer_installed : bool or tuple/list of bool 21 | if False (default), mark as to always use the embedded version (at run time) 22 | if True, mark as to try and use the installed version of package (at run time) 23 | if multiple packages are specified and prefer_installed is a scalar, the value will 24 | be applied for all packages 25 | 26 | py_files_only : bool or tuple/list of bool 27 | if True (default), embed only .py files 28 | if False, embed all files, which can be useful for certain data, fonts, etc, to be present 29 | if multiple packages are specified and py_files_only is a scalar, the value will 30 | be applied for all packages 31 | 32 | outfile : str or pathlib.Path 33 | output file 34 | if None, use infile with extension .embedded.py instead of .py 35 | 36 | Returns 37 | ------- 38 | packages embedded : list 39 | when a package is not found or not embeddable, it is excluded from this list 40 | """ 41 | infile = Path(infile) # for API 42 | if outfile is None: 43 | outfile = infile.parent / (infile.stem + ".embedded" + infile.suffix) 44 | 45 | with open(infile, "r") as f: 46 | inlines = f.read().split("\n") 47 | 48 | inlines_iter = iter(inlines) 49 | inlines = [] 50 | for line in inlines_iter: 51 | if line.startswith("def copy_contents("): 52 | while not line.startswith("del copy_contents"): 53 | line = next(inlines_iter) 54 | else: 55 | inlines.append(line) 56 | 57 | with open(outfile, "w") as out: 58 | if inlines[0].startswith("#!"): 59 | print(inlines.pop(0), file=out) 60 | for lineno, line in enumerate(reversed(inlines)): 61 | if line.startswith("from __future__ import"): 62 | for _ in range(lineno): 63 | print(inlines.pop(0), file=out) 64 | break 65 | 66 | packages = package if isinstance(package, (tuple, list)) else [package] 67 | n = len(packages) 68 | prefer_installeds = prefer_installed if isinstance(prefer_installed, (tuple, list)) else n * [prefer_installed] 69 | py_files_onlys = py_files_only if isinstance(py_files_only, (tuple, list)) else n * [py_files_only] 70 | if len(prefer_installeds) != n: 71 | raise ValueError(f"length of package != length of prefer_installed") 72 | if len(py_files_onlys) != n: 73 | raise ValueError(f"length of package != length of py_files_only") 74 | 75 | embedded_packages = [package for package in packages if _package_location(package)] 76 | 77 | for line in inlines: 78 | if line.startswith("import"): 79 | break 80 | print(line, file=out) 81 | 82 | print("def copy_contents(package, prefer_installed, filecontents):", file=out) 83 | print(" import tempfile", file=out) 84 | print(" import shutil", file=out) 85 | print(" import sys", file=out) 86 | print(" from pathlib import Path", file=out) 87 | print(" import zlib", file=out) 88 | print(" import base64", file=out) 89 | print(" if package in sys.modules:", file=out) 90 | print(" return", file=out) 91 | print(" if prefer_installed:", file=out) 92 | print(" for dir in sys.path:", file=out) 93 | print(" dir = Path(dir)", file=out) 94 | print(" if (dir / package).is_dir() and (dir / package / '__init__.py').is_file():", file=out) 95 | print(" return", file=out) 96 | print(" if (dir / (package + '.py')).is_file():", file=out) 97 | print(" return", file=out) 98 | print(" target_dir = Path(tempfile.gettempdir()) / ('embedded_' + package) ", file=out) 99 | print(" if target_dir.is_dir():", file=out) 100 | print(" shutil.rmtree(target_dir, ignore_errors=True)", file=out) 101 | print(" for file, contents in filecontents:", file=out) 102 | print(" ((target_dir / file).parent).mkdir(parents=True, exist_ok=True)", file=out) 103 | print(" with open(target_dir / file, 'wb') as f:", file=out) 104 | print(" f.write(zlib.decompress(base64.b64decode(contents)))", file=out) 105 | print(" sys.path.insert(prefer_installed * len(sys.path), str(target_dir))", file=out) 106 | 107 | for package, prefer_installed, py_files_only in zip(packages, prefer_installeds, py_files_onlys): 108 | dir = _package_location(package) 109 | 110 | if dir: 111 | print(f"copy_contents(package={repr(package)}, prefer_installed={repr(prefer_installed)}, filecontents=(", file=out) 112 | if dir.is_file(): 113 | files = [dir] 114 | else: 115 | files = dir.rglob("*.py" if py_files_only else "*.*") 116 | for file in files: 117 | if dir.is_file(): 118 | filerel = Path(file.name) 119 | else: 120 | filerel = file.relative_to(dir.parent) 121 | if all(part != "__pycache__" for part in filerel.parts): 122 | with open(file, "rb") as f: 123 | fr = f.read() 124 | print(f" ({repr(filerel.as_posix())},{repr(base64.b64encode(zlib.compress(fr)))}),", file=out) 125 | print("))", file=out) 126 | 127 | print("del copy_contents", file=out) 128 | print(file=out) 129 | started = False 130 | for line in inlines: 131 | if not started: 132 | started = line.startswith("import") 133 | if started: 134 | print(line, file=out) 135 | return embedded_packages 136 | 137 | 138 | def _package_location(package): 139 | for path in sys.path: 140 | path = Path(path) 141 | if (path.stem == "site-packages") or (path.resolve() == Path.cwd().resolve()): 142 | if (path / package).is_dir(): 143 | if (path / package / "__init__.py").is_file(): 144 | return path / package 145 | if (path / (package + ".py")).is_file(): 146 | return path / (package + ".py") 147 | return None 148 | 149 | 150 | def main(): 151 | file_folder = Path(__file__).parent 152 | os.chdir(file_folder / ".." / "peek") 153 | embed_package( 154 | infile="peek.py", package=["executing", "asttokens", "six", "tomli", "colorama"], prefer_installed=False, py_files_only=False, outfile="peek.py" 155 | ) 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | print("done") 161 | 162 | -------------------------------------------------------------------------------- /tools/install peek.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import site 5 | import shutil 6 | import hashlib 7 | import base64 8 | from pathlib import Path 9 | import configparser 10 | import six 11 | import os 12 | from six.moves import urllib 13 | 14 | # import urllib.request 15 | # import urllib.error 16 | 17 | 18 | def _install(files, url=None): 19 | """ 20 | install one file package from GitHub or current directory 21 | 22 | Parameters 23 | ---------- 24 | files : list 25 | files to be installed 26 | the first item (files[0]) will be used as the name of the package'' 27 | optional files should be preceded with an exclamation mark (!) 28 | 29 | url : str 30 | url of the location of the GitHub repository 31 | this will start usually with https://raw.githubusercontent.com/ and end with /master/ 32 | if omitted, the files will be copied from the current directory (not GitHub) 33 | 34 | 35 | Returns 36 | ------- 37 | info : Info instance 38 | info.package : name of the package installed 39 | info.path : name where the package is installed in the site-packages 40 | info.version : version of the package (obtained from .py) 41 | info.files_copied : list of copied files 42 | 43 | Notes 44 | ----- 45 | The program automatically makes the required __init__.py file (unless given in files) and 46 | .dist-info folder with the usual files METADATA, INSTALLER and RECORDS. 47 | As the setup.py is not run, the METADATA is very limited, i.e. is contains just name and version. 48 | 49 | If a __init__.py is in files that file will be used. 50 | Otherwise, an __init__/py file will be generated. In thet case, if a __version__ = statement 51 | is found in the source file, the __version__ will be included in that __init__.py file. 52 | 53 | Version history 54 | --------------- 55 | version 1.0.8 2024-05-06 56 | Changed the way the package is found (again) 57 | 58 | version 1.0.7 2024-05-04 59 | Changed the way the package is found (again) 60 | 61 | version 1.0.6 2024-05-01 62 | If the first source file can't be found in the current working directory, 63 | the program will search one level deep 64 | 65 | version 1.0.5 2020-06-24 66 | Bug with removing the dist-info of packages starting with the same name fixed. 67 | 68 | version 1.0.4 2020-03-29 69 | Linux and ios versions now search in sys.path for site-packages, 70 | whereas other platforms now use site.getsitepackages(). 71 | This is to avoid installation in a roaming directory on Windows. 72 | 73 | version 1.0.2 2020-03-07 74 | modified several open calls to be compatible with Python < 3.6 75 | multipe installation for Pythonista removed. Now installs only in site-packages 76 | 77 | version 1.0.1 2020-03-06 78 | now uses urllib instead of requests to avoid non standard libraries 79 | installation for Pythonista improved 80 | 81 | version 1.0.0 2020-03-04 82 | initial version 83 | 84 | (c)2020 Ruud van der Ham - www.salabim.org 85 | """ 86 | 87 | class Info: 88 | version = "?" 89 | package = "?" 90 | path = "?" 91 | files_copied = [] 92 | 93 | info = Info() 94 | Pythonista = sys.platform == "ios" 95 | if not files: 96 | raise ValueError("no files specified") 97 | if files[0][0] == "!": 98 | raise ValueError("first item in files (sourcefile) may not be optional") 99 | 100 | sourcefile = files[0] 101 | package = Path(sourcefile).stem 102 | 103 | file_folder = Path(__file__).parent 104 | if not (file_folder / sourcefile).is_file(): 105 | top_folder = (file_folder / "..").resolve() 106 | os.chdir(top_folder / package) 107 | 108 | file_contents = {} 109 | for file in files: 110 | optional = file[0] == "!" 111 | if optional: 112 | file = file[1:] 113 | 114 | if url: 115 | try: 116 | with urllib.request.urlopen(url + file) as response: 117 | page = response.read() 118 | 119 | file_contents[file] = page 120 | exists = True 121 | except urllib.error.URLError: 122 | exists = False 123 | 124 | else: 125 | exists = Path(file).is_file() 126 | if exists: 127 | with open(Path(file), "rb") as f: 128 | file_contents[file] = f.read() 129 | 130 | if (not exists) and (not optional): 131 | raise FileNotFoundError(file + " not found. Nothing installed.") 132 | 133 | version = "unknown" 134 | for line in file_contents[sourcefile].decode("utf-8").split("\n"): 135 | line_split = line.split("__version__ =") 136 | if len(line_split) > 1: 137 | raw_version = line_split[-1].strip(" '\"") 138 | version = "" 139 | for c in raw_version: 140 | if c in "0123456789-.": 141 | version += c 142 | else: 143 | break 144 | break 145 | 146 | info.files_copied = list(file_contents.keys()) 147 | info.package = package 148 | info.version = version 149 | 150 | file = "__init__.py" 151 | if file not in file_contents: 152 | file_contents[file] = ("from ." + package + " import *\n").encode() 153 | if version != "unknown": 154 | file_contents[file] += ("from ." + package + " import __version__\n").encode() 155 | if sys.platform.startswith("linux") or (sys.platform == "ios"): 156 | search_in = sys.path 157 | else: 158 | search_in = site.getsitepackages() 159 | 160 | for f in search_in: 161 | sitepackages_path = Path(f) 162 | if sitepackages_path.name == "site-packages" and sitepackages_path.is_dir(): 163 | break 164 | else: 165 | raise ModuleNotFoundError("can't find the site-packages folder") 166 | 167 | path = sitepackages_path / package 168 | info.path = str(path) 169 | 170 | if path.is_file(): 171 | path.unlink() 172 | 173 | if not path.is_dir(): 174 | path.mkdir() 175 | 176 | for file, contents in file_contents.items(): 177 | with (path / file).open("wb") as f: 178 | f.write(contents) 179 | 180 | if Pythonista: 181 | pypi_packages = sitepackages_path / ".pypi_packages" 182 | config = configparser.ConfigParser() 183 | config.read(pypi_packages) 184 | config[package] = {} 185 | config[package]["url"] = "pypi" 186 | config[package]["version"] = version 187 | config[package]["summary"] = "" 188 | config[package]["files"] = path.as_posix() 189 | config[package]["dependency"] = "" 190 | with pypi_packages.open("w") as f: 191 | config.write(f) 192 | else: 193 | for entry in sitepackages_path.glob("*"): 194 | if entry.is_dir(): 195 | if entry.stem.startswith(package + "-") and entry.suffix == ".dist-info": 196 | shutil.rmtree(str(entry)) 197 | path_distinfo = Path(str(path) + "-" + version + ".dist-info") 198 | if not path_distinfo.is_dir(): 199 | path_distinfo.mkdir() 200 | with open(str(path_distinfo / "METADATA"), "w") as f: # make a dummy METADATA file 201 | f.write("Name: " + package + "\n") 202 | f.write("Version: " + version + "\n") 203 | 204 | with open(str(path_distinfo / "INSTALLER"), "w") as f: # make a dummy METADATA file 205 | f.write("github\n") 206 | with open(str(path_distinfo / "RECORD"), "w") as f: 207 | pass # just to create the file to be recorded 208 | 209 | with open(str(path_distinfo / "RECORD"), "w") as record_file: 210 | for p in (path, path_distinfo): 211 | for file in p.glob("**/*"): 212 | if file.is_file(): 213 | name = file.relative_to(sitepackages_path).as_posix() # make sure we have slashes 214 | record_file.write(name + ",") 215 | 216 | if (file.stem == "RECORD" and p == path_distinfo) or ("__pycache__" in name.lower()): 217 | record_file.write(",") 218 | else: 219 | with file.open("rb") as f: 220 | file_contents = f.read() 221 | hash = "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(file_contents).digest()).decode("latin1").rstrip("=") 222 | # hash calculation derived from wheel.py in pip 223 | 224 | length = str(len(file_contents)) 225 | record_file.write(hash + "," + length) 226 | 227 | record_file.write("\n") 228 | 229 | return info 230 | 231 | 232 | if __name__ == "__main__": 233 | info = _install(files="peek.py !changelog.md".split()) 234 | print(info.package + " " + info.version + " successfully installed in " + info.path) 235 | print("files copied: ", ", ".join(info.files_copied)) 236 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ### changelog | peek | like print, but easy. 2 | 3 | For the full documentation, see www.salabim.org/peek . 4 | 5 | #### version 25.0.27 2025-12-18 6 | - `c` is now an alias of `color` (and `col`), so we can say `peek(a, c="red")` 7 | - `cv` is now an alias of `color_value` (and `col_val`), so we can say `peek(a, cv="green")` 8 | - colors can now be numeric, which makes adding colour very compact, like `peek(a, c=3)`, which is equivalent to `peek(a, color="red")`: 9 | - 0 - (reset) 10 | - 1 white 11 | - 2 black 12 | - 3 red 13 | - 4 blue 14 | - 5 green 15 | - 6 yellow 16 | - 7 magenta 17 | - 8 cyan (think tealblue) 18 | 19 | Note that the colour number corresponds to the number of letters in the name (apart from white and black). 20 | A negative colour number represents the dark version, e.g. `peek((a:=123), c=3, cv=-5)` will print `(a:=3)=` in red and `123` in dark_green. 21 | 22 | #### version 25.0.26 2025-11-09 23 | 24 | - test for globals, locals and vars now done with 'is' instead of '==', after a problematic error with istr.__eq__ . 25 | - test suite now explicitly defines colors, instead of a clever patch loop, to get less ruff warnings reported. 26 | 27 | #### version 25.0.25 2025-11-08 28 | 29 | - the end attribute was printed/returned without any color, even if color was specified, which is not what was expected. 30 | now, the end attribute is always colored (if color is active), unless end is "\n", which is nearly always the case. 31 | tests are updated accordingly. 32 | 33 | #### version 25.0.24 2025-09-14 34 | 35 | - under Pythonista, `peek.stop` crashed the Pythonista app. Fixed by calling `sys.exit()` instead of raising a `SystemExit` exception (applies only to Pythonista). 36 | - under Pyodide, `peek.stop` now raises an `Exception` exception as `sys.exit()` does not work as expected here (applies only to Pyodide/xlwings lite). 37 | 38 | #### version 25.0.23 2025-09-06 39 | 40 | - peek tried to import tomlib instead of tomllib. Fixed. 41 | 42 | #### version 25.0.22 2025-08-27 43 | 44 | - With this version, `\r` also works under Pythonista. Note that `\r` has to be at the start of the output. 45 | Due to limitations of Pythonista, it is not possible to use `end="\r"` there. 46 | So, this will work now as expected under Pythonista: 47 | 48 | ``` 49 | import time 50 | for i in range(50): 51 | peek({time.time(), i, " ", end="", prefix='\r) 52 | time.sleep(0.1) 53 | peek.print() 54 | ``` 55 | or 56 | ``` 57 | import time 58 | for i in range(50): 59 | peek.print(f"\rtime = {time.time():10.2f}", end="") 60 | time.sleep(0.1) 61 | peek.print() 62 | ``` 63 | 64 | #### version 25.0.21 2025-08-24 65 | 66 | - Reversioned. Just as a side note: reading settings from the environment variables can also be very useful under Pythonista. 67 | 68 | #### version 25.0.19 2025-05-10 69 | 70 | - It is now possible to automatically adjust the line length to the current terminal size, by setting `line_length` (or `ll`) to 0 or 'terminal_width'. Note that not all terminals correctly return the actual width. 71 | (The terminal size is determined by calling `shutil.get_terminal_size().columns`) 72 | 73 | #### version 25.0.18 2025-05-09 74 | 75 | - In addition to overriding defaults with a *peek.toml* file, the user can now specify these overrides as environment variables. 76 | They should be specified as variable `peek.`, like `peek.line_length` and `peek.color`. If the value is a string, the value should be quoted or double-quoted. Note that the environment variables are read *before* reading a *peek.toml* file. 77 | This functionality is particularly useful for using peek in xlwings lite, as no local file system to store a toml file exists on that platform. 78 | - The new attribute `max_lines` or `ml` can be used to specify the maximum number of lines acceptable. If the output doesn't fit the given max_lines, [abbreviated] will be displayed at the end. By default, this is 1000000. 79 | This can be particularly useful with xlwings lite, as printing many lines to the console may take a long, very long time. So, an acceptable setting under xlwings lite might be 6. It is recommended to add a max_lines value to the environment variables, then. 80 | - On pyodide platforms (e.g. xlwings lite), ANSI color codes are now suppressed with the `use_color` attribute. The default for `use_color` is False on pyodide platforms, True otherwise. 81 | 82 | #### version 25.0.17 2025-05-08 83 | 84 | - Introduced `peek.reset()`, which can be useful on platforms (like Pythonista and xlwings lite) that do not reinitialise when a new run is started. 85 | 86 | #### version 25.0.16 2025-05-04 87 | 88 | - Minor bug when using peek(as_str=True) and colors fixed. 89 | 90 | #### version 25.0.15 2025-04-15 91 | 92 | - When run under pyodide (e.g. with xlwings lite), the pyperclip package requirement caused an error (as there's no wheel for that pyperclip). 93 | Therefore, pyperclip is not specified as a requirement anymore. 94 | - Under pyodide (e.g. with xlwings lite), colors are suppressed. 95 | 96 | #### version 25.0.14 2025-04-09 97 | 98 | - Colors were not applied if peek was called without values (including as decorator or context manager). Fixed. 99 | 100 | #### version 25.0.13 2025-03-19 101 | 102 | - It is now properly documented (and tested) that under Python 3.9 the underscore_numbers attribute is ignored. 103 | - Some tests did fail incorrectly under Python 3.10. Now these tests are just skipped. 104 | - Although Python >= 3.9 was already a requirement from 25.0.5 , the pyproject.toml file was not updated accordingly. Fixed. 105 | - One of the tests still checked for behaviour under Python < 3.9. Removed that check. 106 | 107 | #### version 25.0.12 2025-03-11 108 | 109 | - Introduced `peek.stop` or `peek.stop()` to stop a program conditionally. 110 | 111 | ``` 112 | peek.enabled = False 113 | peek(12) 114 | peek.stop 115 | peek.enabled = True 116 | peek(13) 117 | peek.stop 118 | peek(14) 119 | ``` 120 | This will print: 121 | ``` 122 | 13 123 | stopped by peek.stop 124 | ``` 125 | and then stop execution. 126 | 127 | #### version 25.0.11 2025-03-09 128 | 129 | - Some minor changes to readme.md . 130 | 131 | #### version 25.0.10 2025-02-11 132 | 133 | - Some minor changes to readme.md . 134 | 135 | #### version 25.0.9 2025-02-04 136 | 137 | - Accessing lines in code resulted sometimes in an IndexError under Pythonista. Fixed. 138 | - Changed the required version of executing into 2.2.0 (was 2.1.0). 139 | 140 | #### version 25.0.7 2025-01-19 141 | - Overhaul of color enable/disable functionality: 142 | `as_colored_str` parameter does not exist anymore. Just `as_str`, combined with `use_color = True` will do the job. 143 | "stdout_no_color" for output does not exist anymore. "stdout", combined with `use_color = False` will do the job. 144 | The new attribute `use_color` can be used to control whether ANSI escapes will be emitted. By default `use_color` is True, so given colors will be observed. 145 | 146 | #### version 25.0.6 2025-01-18 147 | 148 | - peek now offers direct access to ANSI color escape sequences with `peek.ANSI.black`, `peek.ANSI.white`, `peek.ANSI.red`, `peek.ANSI.green`, `peek.ANSI.blue`, `peek.ANSI.cyan`, `peek.ANSI.magenta`, `peek.ANSI.yellow`, `peek.ANSI.light_black`, `peek.ANSI.light_white`, `peek.ANSI.light_red`, `peek.ANSI.light_green`, `peek.ANSI.light_blue`, `peek.ANSI.light_cyan`, `peek.ANSI.light_magenta`, `peek.ANSI.light_yellow` and `peek.reset`. 149 | 150 | E.g. 151 | 152 | ``` 153 | peek(repr(peek.ANSI.red)) 154 | ``` 155 | 156 | will show 157 | 158 | ``` 159 | repr(peek.ANSI.red)='\x1b[1;31m' 160 | ``` 161 | 162 | #### version 25.0.5 2025-01-17 163 | 164 | - peek is not supported on Python < 3.9 anymore. Updated the pyproject.toml file accordingly. 165 | 166 | #### version 25.0.4 2025-01-17 167 | - Bug when running under Pythonista fixed. 168 | - Left over from an internal debug print removed. 169 | 170 | #### version 25.0.3 2025-01-15 171 | - peeking all local or all global variables with the functionality as introduced in 25.0.2 didn't work properly if peek was called directly (not via the PeekModule). Fixed by introducing an optional _via_module to peek. 172 | - peek no longer issues 'No source' warnings as it now automatically falls back to printing only the value in case the source can't be found or has changed. 173 | - peek now uses light colors for black, white, ..., yellow, to make the output easier to read. If you would like the 'normal' colors, use dark_black, dark_white, ..., dark_yellow. See the read.me for an overview of the colors. 174 | - It is now also possible to return peek's output as a string with the embedded ANSI color escape string(s). This is done by setting the `as_colored_str` argument to True: 175 | 176 | ``` 177 | hello = "world" 178 | s = peek(hello, color="red", color_value="green", as_colored_str=True) 179 | print(repr(s), end="") 180 | ``` 181 | 182 | prints 183 | 184 | ``` 185 | "\x1b[1;31mhello=\x1b[1;32m'world'\x1b[1;31m\n\x1b[0m" 186 | ``` 187 | 188 | 189 | #### version 25.0.2 2025-01-13 190 | - Introduced the possibility to print all local or all global variables. 191 | 192 | To do that, just put `locals` or `globals` in the call to peek: 193 | 194 | ``` 195 | peek(locals) 196 | ``` 197 | will print all local variables, apart from those starting with `__` 198 | 199 | Likewise, 200 | ``` 201 | peek(globals) 202 | ``` 203 | will print all global variables, apart from those starting with `__` 204 | 205 | > [!IMPORTANT] 206 | > 207 | > You should not add parentheses after `locals` or `globals` for peek to work properly! 208 | #### version 25.0.1 2025-01-09 209 | 210 | - Introduced the format attribute: 211 | 212 | With the format attribute, it is possible to apply a format specifier to each of the values to be printed, like 213 | 214 | ``` 215 | test_float = 1.3 216 | peek(test_float, format="06.3f") 217 | ``` 218 | 219 | This will print 220 | 221 | ``` 222 | test_float=01.300 223 | ``` 224 | 225 | The format should be like the Python format specifiers, with or without the `:` prefix, like `"6.3f"`, `">10"`, `"06d"`, `:6.3d`. 226 | It is also possible to use the `!` format specifier: `"!r"`, `"!r:>10"`. 227 | 228 | If format is the null string (`""`) (the default), this functionality is skipped completely. 229 | 230 | It is also possible to use a list (or tuple) of format specifiers, which are tried in succession. If they all fail, the 'normal' serializer will be used. 231 | 232 | ``` 233 | test_float = 1.3 234 | test_integer=10 235 | test_string = "test" 236 | test_dict=dict(one=1, two=2) 237 | peek(test_float, test_integer, test_string, test_dict, format=["04d", "06.3f", ">10"]) 238 | ``` 239 | 240 | will result in 241 | 242 | ``` 243 | test_float=01.300, test_integer=0010, test_string= test, test_dict={'one': 1, 'two': 2} 244 | ``` 245 | 246 | Of course, format may be put in a peek.toml file. 247 | 248 | #### version 25.0.0 2025-01-07 249 | 250 | * internal reorganization: all methods and constants are now in the _Peek class. 251 | 252 | 253 | #### version 24.0.5 2024-12-28 254 | 255 | * peek has a new attribute: `print_like` (alias `print`), which is False by default. If true, peek behaves like `peek.print`. 256 | 257 | This makes it possible to use `peek` as were it `print` , but with colors, filtering, optional line numbers, timing info, and enabling/disabling. 258 | 259 | E.g. if we first set 260 | 261 | ``` 262 | peek.print_like = True 263 | ``` 264 | and then 265 | ``` 266 | peek(12, f"{min(1, 2)=}", list(range(4)) 267 | ``` 268 | it will print 269 | ```1 270 | 2 min(1, 2)=1 [0, 1, 2, 3] 271 | ``` 272 | `sep` and `end` are supported, so after setting print_like to True: 273 | 274 | ``` 275 | peek(12, f"{min(1, 2)=}", list(range(4), sep="|",end="!\n")) 276 | ``` 277 | will print 278 | ``` 279 | 12|min(1, 2)=1|[0, 1, 2, 3]! 280 | ``` 281 | 282 | 283 | It is possible to use `print_like` in a call to peek, like 284 | ``` 285 | peek(12, 13, 14, print_like=True) 286 | ``` 287 | , but it might be more convenient to use 288 | ``` 289 | peek.print(12, 13, 14) 290 | ``` 291 | > [!TIP] 292 | > 293 | > Of course, `print_like` can be set in a **peek.toml** file. 294 | 295 | * Internal only: `peek.print` now just delegates to` peek(print_like=True)` 296 | 297 | #### version 24.0.4 2024-12-26 298 | 299 | * Introduced the `end` attribute, which works like the end parameter of print. By default, `end` is "\n". 300 | This can be useful to have several peek outputs on one line, like: 301 | 302 | ``` 303 | for i in range(5): 304 | peek(i*i, end=' ') 305 | peek('') 306 | ``` 307 | Maybe more useful is to show the output change on the same line, e.g. a status. 308 | ``` 309 | import time 310 | for i in range(50): 311 | peek(f"time {time.time()}",end="\r") 312 | time.sleep(0.1) 313 | peek('') 314 | ``` 315 | Note that `\r` does not work under Pythonista. 316 | 317 | * Introduced `peek.print` which allows peek to be used as an alternative to print. Note that `peek.print` obeys the `color`, `filter`, `enabled` and `as_str` and `output`. 318 | 319 | So we can say 320 | ``` 321 | peek.color = "red" 322 | peek.filter = "level==1" 323 | peek.print(f"{max(1, 2)=}") 324 | peek.print(f"{min(1, 2)=}", level=1) 325 | ``` 326 | will print 327 | ``` 328 | min(1, 2)=1 329 | ``` 330 | in red. 331 | 332 | In order to behave similar to print, peek has an extra attribute, `separator_print` or `sepp`. This attribute (default " ") will be used when `peek.printing`. 333 | When calling `peek.print`, `sep` may be used instead. So 334 | 335 | ``` 336 | peek.sepp = "|" 337 | peek.print("test") 338 | ``` 339 | Has the same effect as 340 | ``` 341 | peek.print("test", sep="|") 342 | ``` 343 | and 344 | ``` 345 | peek.print("test", sepp="|") 346 | ``` 347 | but not the same as 348 | ``` 349 | peek.sep = "|" # sets the 'normal' peek separator 350 | ``` 351 | 352 | * `enforce_line_length` attribute phased out because of limited use 353 | #### version 24.0.3 2024-12-24 354 | 355 | * When both `peek.color` and `peek.color_value` were "-" or "", ansi escape sequences were still emitted. From now on, peek will suppress these. 356 | * From now on `peek.color` may also be "". It acts exactly as "-". 357 | Just for your information, if `peek color_value` is "" it means use the `peek.color`, wheras if `peek.color_value` is "-", it means switch back to no color. 358 | 359 | #### version 24.0.2 2024-12-23 360 | 361 | * Bug when using peek with an *istr* (see www.salabim.org/istr) and `quote_string = False`. Fixed. 362 | * Some minor code cleanups. 363 | 364 | #### version 24.0.1 2024-12-22 365 | 366 | * Some more refactoring to avoid code duplication. 367 | 368 | * Changed several TypeError and ValueError exception to the more logical and consistent AttributeError 369 | 370 | * Implemented `repr` and `str`, where delta is the initial value (which required an alternative way to store delta) 371 | 372 | * Added some more test in the PyTest script 373 | 374 | #### version 24.0.0 2024-12-20 375 | 376 | * Completely refactored the way peek defaults and arguments are handled, leading to much more compact and 377 | better maintainable code. Also errors are better captured. 378 | 379 | * This change makes it also possible to check for incorrect assignment of peek's attribute, like `peek.colour = 'red'` 380 | 381 | * The show_level and show_color methods are replaced by the generic filter attribute, allowing more sophisticated filtering. E.g. 382 | ``` 383 | peek.filter('color not in blue' and level >= 2') 384 | ``` 385 | It is even possible to get access to all peek attributes in the filter condition, e.g. 386 | ``` 387 | peek.filter('delta > 2') 388 | ``` 389 | Note that peek checks the validity of a filter expression at definition time. 390 | 391 | * to_clipboard is not anymore just an argument of peek, but is now an attribute. As a consequence, 392 | the method `to_clipboard` has been renamed. It is now `copy_to_clipboard`. 393 | 394 | * to_clipboard will now put the *last* value on the clipboard (was the *first*) 395 | 396 | * Introduced the *quote_string* attribute. If this attribute is set to False, strings will be displayed without surrounding quotes 397 | (like str). When True (the default), strings will be displayed with surrounding quotes (like repr). E.g. 398 | 399 | ``` 400 | test='test' 401 | peek('==>', test) 402 | peek('==>', test, quote_string=False) 403 | ``` 404 | 405 | This will print: 406 | 407 | ``` 408 | '==>', test='test' 409 | ==>, test=test 410 | ``` 411 | 412 | * provided phased out because of limited use 413 | 414 | * assert_ phased out because of limited use 415 | 416 | * peek.toml is now only read once upon importing peek, which is more efficient and stable 417 | 418 | * changed to calendar versioning, so this is version 24.0.0 . 419 | 420 | 421 | #### version 1.8.8 2024-12-14 422 | 423 | * color_value may now be also the null string ("") to indicate to use the color for the values as well. This also the default now. 424 | 425 | #### version 1.8.7 2024-12-13 426 | 427 | * introduced `peek.show_color()`, which makes it possible to show only output of given color(s). 428 | It is also possible to exclude given color(s). 429 | This works similar to `peek.show_level()`, including as a context manager. 430 | * changed the *no color* attribute from `""` to `"-"` (this change was necessary for `peek.show_color()` to also include *no color*). So, for instance, `peek.show_color("not -")` will only show colored output. 431 | * `peek.output` may now be **"stdout_nocolor"**, which makes that colors are ignored (this is primarily useful for tests). 432 | * micro optimization by not inserting any ansi escape sequences if neither color nor color_value is specified. 433 | * the build process will now automatically insert the latest version for each of the requirements. 434 | 435 | #### version 1.8.4 2024-12-11 436 | 437 | * all required modules in the pyproject.toml file now have a mininal version number (all the latest as of now). 438 | This is rather important as particularly older versions of *executing* may not compatible with *peek*. 439 | (inspired by an issue reported by Kirby James) 440 | 441 | #### version 1.8.3 2024-12-10 442 | 443 | * added an alternative way to copy peek output to the clipboard. 444 | From now on a call to peek has an optional keyword argument, *to_clipboard*: 445 | 446 | - If to_clipboard==False (the default), nothing is copied to the clipboard. 447 | - If to_clipboard==True, the *value* of the the *first* parameter will be copied to the clipboard. The output itself is as usual. 448 | 449 | Examples: 450 | 451 | ``` 452 | part1 = 200 453 | extra = "extra" 454 | peek(part1, extra, to_clipboard=True) 455 | # will print part1=200, extra='extra' and copy 200 to the clipboard 456 | peek(200, to_clipboard=True)\ 457 | # will print 200 and copy 200 to the clipboard 458 | peek(to_clipboard=True) 459 | # will print #5 (or similar) and empty the clipboard 460 | ``` 461 | 462 | Note that *to_clipboard* is not a peek attribute and can only be used when calling `peek`, 463 | If as_str==True, to_clipboard is ignored. 464 | 465 | #### version 1.8.2 2024-12-10 466 | 467 | * updated *pyproject.toml* to correct the project.url information 468 | (inspired by a comment by Erik OShaughnessy) 469 | 470 | 471 | 472 | #### version 1.8.1 2024-12-09 473 | 474 | * introduced the possibility to copy peek output to the clipboard. 475 | 476 | Therefore peek has now a method `to_clipboard` which accepts a value to be copied to the clipboard. 477 | So, 478 | 479 | ``` 480 | part1 = 1234 481 | peek.to_clipboard(part1) 482 | ``` 483 | will copy `1234` to the clipboard and write `copied to clipboard: 1234` to the console. 484 | If the confirmation message is not wanted, just add confirm=False, like 485 | 486 | ``` 487 | peek.to_clipboard(part1, confirm=False) 488 | ``` 489 | Implementation detail: this functionality uses pyperclip, apart from under Pythonista, whre the 490 | builtin clipboard module is used. 491 | 492 | This functionality is particularly useful for entering an answer of an *Advent of Code* solution to the site. 493 | 494 | (inspired by a comment by Geir Arne Hjelle) 495 | 496 | #### version 1.8.0 2024-12-09 497 | 498 | * `show_level` is now a method 499 | 500 | * `show_level` is called slightly different as it is a method with a number (usually 1) of arguments. 501 | 502 | Each parameter may be: 503 | 504 | * a float value or 505 | 506 | - a string with the format *from* - *to* 507 | , where both *from* and *to* are optional. If *from* is omitted, -1E30 is assumed. If *to* is omitted, 1E30 is assumed. 508 | Negative values have to be parenthesized. 509 | 510 | Examples: 511 | - `peek.show_level (1)` ==> show level 1 512 | - `peek.show_level (1, -3)` ==> show level 1 and level -3 513 | - `peek.show_level ("1-2")` ==> show level between 1 and 2 514 | - `peek.show_level("-")` ==> show all levels 515 | - `peek.show_level("")` ==> show no levels 516 | - `peek.show_level("1-")`==> show all levels >= 1 517 | - `peek.show_level("-10.2")`==> show all levels <=10.2 518 | - `peek.show_level(1, 2, "5-7", "10-")` ==> show levels 1, 2, between 5 and 7 (inclusive) and >= 10 519 | - `peek.show_level((-3)-3")` ==> show levels between -3 and 3 (inclusive) 520 | - `peek.show_level()` ==> returns the current show_level 521 | 522 | * show_level can also be called with a minimum and/or a maximum value, e.g. 523 | 524 | - `peek.show_level(min=1)` ==> show all levels >= 1 525 | - `peek.show_level(max=10.2)` ==> show all levels <= 10.2 526 | - `peek.show_level(min=1, max=10)` ==> show all levels between 1 and 10 (inclusive) 527 | 528 | Note that min or max cannot be combined with a specifier as above 529 | 530 | * `show_level` can now be used as a context manager as well: 531 | 532 | ``` 533 | with peek.show_level(1): 534 | peek(1, level=1) 535 | peek(2, level=2) 536 | ``` 537 | 538 | This will print one line with`1` only. 539 | 540 | * color_value introduced. When specified, this is the color with which values are presented, e.g. 541 | ``` 542 | test="test" 543 | peek(test, color="red", color_value="blue") 544 | ``` 545 | 546 | * colors on Pythonista are now handled via an ansi table lookup, thus being more reliable 547 | 548 | * performance of peek when level is not to be shown or enabled==False significantly improved. 549 | 550 | #### version 1.7.0 2024-12-03 551 | 552 | * `show_level` element may be a range now, like "3-5', "-5", "3-". 553 | * if `level` is the null string now, output is always suppressed. 554 | * from this version on, it is required to have the following modules installed: `asttokens`, `colorama`, `executing`, `six`, `tomli`. These packages are in the pyproject.toml's dependencies, so normally these will be auto installed. 555 | The reason for this change is that some (potential) users didn't like the encrypted module contents. 556 | 557 | #### version 1.6.2 2024-12-02 558 | 559 | * bug with Python < 3.13. Fixed. 560 | 561 | #### version 1.6.1 2024-12-02 562 | 563 | * peek now has an advanced level system, which can be very practical to enable and disable debug information. 564 | 565 | See the readme file (e.g. on www.salabim.peek/changelog) for details. 566 | 567 | #### version 1.6.0 2024-11-29 568 | 569 | * peek now supports coloring of peek lines. Therefore, the attribute 'color' has been added. 570 | 571 | The following colors are available: 572 | 573 | - black 574 | - red 575 | - green 576 | - blue 577 | - yellow 578 | - cyan 579 | - magenta 580 | - white 581 | 582 | So, for instance, `peek(message, color="red")` or `peek.color = "green"` 583 | 584 | The attribute color has an abbreviated form: `col`, so, for instance, `peek_green = peek.new(col = "green")` 585 | 586 | Resetting the color can be done with the null string: `peek.color = ""`. 587 | 588 | The color attribute can, of course, also be set in a peek.toml file. 589 | 590 | * changed the style badge in the readme.md file from ![Black](https://img.shields.io/badge/code%20style-black-000000.svg) to ![ruff](https://img.shields.io/badge/style-ruff-41B5BE?style=flat) . 591 | 592 | #### version 1.5.2 2024-11-19 593 | 594 | * peek now uses a peek.toml file for customization, instead of a peek.json file. 595 | Also, the directory hierarchy is now searched for rather than sys.path. 596 | 597 | * default line length is (again) 80. This change was made because it is now very easy to change the default line length with a toml file. 598 | 599 | #### version 1.5.1 2024-11-17 600 | 601 | * peek is now also added to builtins, which means that you can just import it anywhere and it will become available in all modules. 602 | 603 | #### version 1.5.0 2024-11-14 604 | 605 | * default line length is now 160 (was 80) 606 | 607 | * removed the fast disable functionality as from now only peek() without positional arguments is allowed 608 | for decorator and context manager 609 | 610 | * phased out decorator/d and context_manager/cm parameters, because of limited use. 611 | 612 | * phased out fast disabling logic, as that is not relevant anymore 613 | 614 | * for decorator and context manager, positional arguments are not allowed. Not obeying 615 | this will result in a -possibly quite cryptic- error message 616 | 617 | * @peek without parentheses was always discouraged, and is not allowed anymore 618 | 619 | * use as decorator or context manager is not allowed in the REPL anymore 620 | 621 | 622 | #### version 1.4.5 2024-11-07 623 | 624 | * finally managed to support the `import peek` functionality correctly. 625 | 626 | #### version 1.4.4 2024-11-06 627 | 628 | * the new method of importing with `import peek` didn't work properly. So it is required to use `from peek import peek` 629 | 630 | #### version 1.4.3 2024-11-06 631 | 632 | * Output of peek now goes to stdout by default. 633 | * Source of pprint not included anymore. 634 | The only consequence is that under Pyton<=3.7: 635 | * dicts are always sorted under Python <=3.7 (regardless of the sort_dicts attribute) 636 | * numbers cannot be underscored under Python <= 3.7 (regardless of the underscore_numbers attribute) 637 | * The default prefix is now "" (the null string) (was: "peek| ") 638 | * The equals_separator is now "=" (was ": ") 639 | 640 | #### version 1.4.2 2024-11-04 641 | 642 | * Initial release. 643 | 644 | * Rebrand of ycecream with several enhancement, particularly the `import peek` functionality. 645 | 646 | -------------------------------------------------------------------------------- /peek/peek.py: -------------------------------------------------------------------------------- 1 | # _ __ ___ ___ | | __ 2 | # | '_ \ / _ \ / _ \| |/ / 3 | # | |_) || __/| __/| < 4 | # | .__/ \___| \___||_|\_\ 5 | # |_| like print, but easy. 6 | 7 | 8 | """ 9 | See https://github.com/salabim/peek for details 10 | 11 | (c)2025 Ruud van der Ham - rt.van.der.ham@gmail.com 12 | 13 | Inspired by IceCream "Never use print() to debug again". 14 | Also contains some of the original code. 15 | IceCream was written by Ansgar Grunseid / grunseid.com / grunseid@gmail.com 16 | """ 17 | 18 | import inspect 19 | import sys 20 | import datetime 21 | import time 22 | import textwrap 23 | import contextlib 24 | import functools 25 | import logging 26 | import collections 27 | import numbers 28 | import ast 29 | import os 30 | import traceback 31 | import executing 32 | import types 33 | import pprint 34 | import builtins 35 | import shutil 36 | 37 | __version__ = "25.0.27" 38 | 39 | from pathlib import Path 40 | 41 | Pythonista = sys.platform == "ios" 42 | Pyodide = "pyodide" in sys.modules 43 | 44 | 45 | if Pythonista: 46 | import console 47 | else: 48 | import colorama 49 | 50 | colorama.just_fix_windows_console() 51 | 52 | try: 53 | import tomllib 54 | except ModuleNotFoundError: 55 | import tomli as tomllib 56 | 57 | 58 | class _Peek: 59 | name_alias_default = ( 60 | # name, alias, default value, print_like accept? 61 | ("color", "col", "-"), 62 | ("color_value", "col_val", ""), 63 | ("compact", "", False), 64 | ("context_separator", "cs", " ==> "), 65 | ("delta", "", 0), 66 | ("depth", "", 1000000), 67 | ("enabled", "", True), 68 | ("end", "", "\n"), 69 | ("equals_separator", "", "="), 70 | ("filter", "f", ""), 71 | ("format", "fmt", ""), 72 | ("indent", "", 1), 73 | ("level", "lvl", 0), 74 | ("line_length", "ll", 80), 75 | ("max_lines", "ml", 1000000), 76 | ("output", "", "stdout"), 77 | ("prefix", "pr", ""), 78 | ("print_like", "print", False), 79 | ("quote_string", "qs", True), 80 | ("return_none", "", False), 81 | ("separator", "sep", ", "), 82 | ("separator_print", "sepp", " "), 83 | ("serialize", "", pprint.pformat), 84 | ("show_delta", "sd", False), 85 | ("show_enter", "se", True), 86 | ("show_exit", "sx", True), 87 | ("show_line_number", "sln", False), 88 | ("show_time", "st", False), 89 | ("show_traceback", "", False), 90 | ("sort_dicts", "", False), 91 | ("to_clipboard", "clip", False), 92 | ("underscore_numbers", "un", False), 93 | ("use_color", "", False if Pyodide else True), 94 | ("values_only", "vo", False), 95 | ("values_only_for_fstrings", "voff", False), 96 | ("wrap_indent", "", " "), 97 | ) 98 | alias_name = {alias: name for (name, alias, default) in name_alias_default if alias} 99 | name_alias = {name: alias for (name, alias, default) in name_alias_default} 100 | name_default = {name: default for (name, alias, default) in name_alias_default} 101 | alias_default = {alias: default for (name, alias, default) in name_alias_default if alias} 102 | name_and_alias_default = name_default | alias_default 103 | 104 | _fixed_perf_counter = None 105 | 106 | _color_name_to_ANSI = dict( 107 | dark_black="\033[0;30m", 108 | dark_red="\033[0;31m", 109 | dark_green="\033[0;32m", 110 | dark_yellow="\033[0;33m", 111 | dark_blue="\033[0;34m", 112 | dark_magenta="\033[0;35m", 113 | dark_cyan="\033[0;36m", 114 | dark_white="\033[0;37m", 115 | black="\033[1;30m", 116 | red="\033[1;31m", 117 | green="\033[1;32m", 118 | yellow="\033[1;33m", 119 | blue="\033[1;34m", 120 | magenta="\033[1;35m", 121 | cyan="\033[1;36m", 122 | white="\033[1;37m", 123 | reset="\033[0m", 124 | ) 125 | _color_name_to_ANSI["-"] = _color_name_to_ANSI["reset"] 126 | _color_name_to_ANSI[""] = _color_name_to_ANSI["reset"] 127 | 128 | _ANSI_to_rgb = { 129 | "\033[1;30m": (51, 51, 51), 130 | "\033[1;31m": (255, 0, 0), 131 | "\033[1;32m": (0, 255, 0), 132 | "\033[1;33m": (255, 255, 0), 133 | "\033[1;34m": (0, 178, 255), 134 | "\033[1;35m": (255, 0, 255), 135 | "\033[1;36m": (0, 255, 255), 136 | "\033[1;37m": (255, 255, 255), 137 | "\033[0;30m": (76, 76, 76), 138 | "\033[0;31m": (178, 0, 0), 139 | "\033[0;32m": (0, 178, 0), 140 | "\033[0;33m": (178, 178, 0), 141 | "\033[0;34m": (0, 89, 255), 142 | "\033[0;35m": (178, 0, 178), 143 | "\033[0;36m": (0, 178, 178), 144 | "\033[0;37m": (178, 178, 178), 145 | "\033[0m": (), 146 | } 147 | id_to_color = { 148 | 0: "-", 149 | 1: "white", 150 | 2: "black", 151 | 3: "red", 152 | 4: "blue", 153 | 5: "green", 154 | 6: "yellow", 155 | 7: "magenta", 156 | 8: "cyan"} 157 | id_to_color.update({-id: f"dark_{name}" for id, name in id_to_color.items() if id}) 158 | 159 | ANSI = types.SimpleNamespace(**_color_name_to_ANSI) 160 | 161 | codes = {} 162 | 163 | @staticmethod 164 | def de_alias(name): 165 | if name == "c": 166 | return "color" 167 | if name == "cv": 168 | return "color_value" 169 | return _Peek.alias_name.get(name, name) 170 | 171 | @staticmethod 172 | def check_validity(name, value): 173 | name_org = name 174 | name = _Peek.de_alias(name) 175 | if name not in _Peek.name_default: 176 | raise AttributeError(f"attribute {name_org} not allowed{_Peek.in_read_toml_message}") 177 | 178 | if value is None: 179 | return 180 | if name == "output": 181 | if callable(value): 182 | return 183 | if isinstance(value, (str, Path)): 184 | return 185 | try: 186 | value.write("") 187 | return 188 | except Exception: 189 | pass 190 | raise AttributeError("output should be a callable, str, Path or open text file.") 191 | 192 | elif name == "serialize": 193 | if callable(value): 194 | return 195 | 196 | elif name in ("color", "color_value"): 197 | if isinstance(value, str) and value in _Peek._color_name_to_ANSI: 198 | return 199 | if isinstance(value, int) and value in _Peek.id_to_color: 200 | return 201 | 202 | elif name == "delta": 203 | if isinstance(value, numbers.Number): 204 | return 205 | 206 | elif name == "line_length": 207 | if (isinstance(value, numbers.Number) and value >= 0) or value == "terminal_width": 208 | return 209 | 210 | elif name == "indent": 211 | if isinstance(value, numbers.Number) and value >= 0: 212 | return 213 | 214 | elif name == "level": 215 | if isinstance(value, numbers.Number): 216 | return 217 | 218 | elif name == "max_lines": 219 | if isinstance(value, numbers.Number) and value > 0: 220 | return 221 | 222 | elif name == "wrap_indent": 223 | if isinstance(value, str): 224 | return 225 | if isinstance(value, numbers.Number): 226 | if value > 0: 227 | return 228 | 229 | elif name == "format": 230 | if isinstance(value, str): 231 | return 232 | try: 233 | if all(isinstance(sub_format, str) for sub_format in value): 234 | return 235 | except TypeError: 236 | ... 237 | 238 | elif name == "filter": 239 | if value.strip() == "": 240 | return 241 | try: 242 | eval(value, _Peek.name_and_alias_default) 243 | return 244 | except Exception: 245 | ... 246 | 247 | else: 248 | return 249 | 250 | raise AttributeError(f"incorrect {name_org}: {repr(value)}{_Peek.in_read_toml_message}") 251 | 252 | @staticmethod 253 | def spec_to_attributes(**kwargs): 254 | result = {} 255 | for name, value in kwargs.items(): 256 | if _Peek.alias_name.get(name, "") in kwargs: 257 | raise AttributeError(f"not allowed to use {name} and {_Peek.alias_name.get(name)} both.") 258 | _Peek.check_validity(name, value) 259 | name = _Peek.de_alias(name) 260 | if name in ("color", "color_value") and isinstance(value, int): 261 | value = _Peek.id_to_color[value] 262 | if name == "delta" and value is not None: 263 | result["delta1"] = _Peek.perf_counter() 264 | result[name] = value 265 | return result 266 | 267 | @staticmethod 268 | def read_toml(): 269 | result = {} 270 | _Peek.in_read_toml_message = f" in reading environment variable(s)" 271 | for environment_variable, value in os.environ.items(): 272 | environment_variable = environment_variable.lower() 273 | value = value.strip() 274 | if environment_variable.startswith("peek."): 275 | attribute = environment_variable[5:] 276 | if (value.startswith("'") and value.endswith("'")) or (value.startswith('"') and value.endswith('"')): 277 | value = value[1:-1] 278 | elif value.lower() == "true": 279 | value = True 280 | elif value.lower() == "false": 281 | value = False 282 | else: 283 | try: 284 | value = int(value) 285 | except ValueError: 286 | ... 287 | 288 | result[attribute] = value 289 | 290 | this_path = Path(".").resolve() 291 | for i in range(len(this_path.parts), 0, -1): 292 | toml_file = Path(this_path.parts[0]).joinpath(*this_path.parts[1:i], "peek.toml") 293 | if toml_file.is_file(): 294 | _Peek.in_read_toml_message = f" in reading {toml_file} or environment variable(s)" 295 | with open(toml_file, "r") as f: 296 | config_as_str = f.read() 297 | result.update(tomllib.loads(config_as_str)) 298 | break 299 | return result 300 | 301 | @staticmethod 302 | def print_pythonista_color(s, end="\n", file=sys.stdout): 303 | cached = "" 304 | while s: 305 | for ansi, rgb in _Peek._ANSI_to_rgb.items(): 306 | if s.startswith(ansi): 307 | if cached: 308 | print(cached, end="", file=file) 309 | cached = "" 310 | console.set_color(*tuple(v / 255 for v in rgb)) 311 | s = s[len(ansi) :] 312 | break 313 | else: 314 | cached += s[0] 315 | s = s[1:] 316 | print(cached + end, end="", file=file) 317 | 318 | def print_without_color(s, end="\n", file=sys.stdout): 319 | while s: 320 | for ansi, rgb in _Peek._ANSI_to_rgb.items(): 321 | if s.startswith(ansi): 322 | s = s[len(ansi) :] 323 | break 324 | else: 325 | print(s[0], end="", file=file) 326 | s = s[1:] 327 | print("", end=end, file=file) 328 | 329 | @staticmethod 330 | def return_args(args, return_none): 331 | if return_none: 332 | return None 333 | if len(args) == 0: 334 | return None 335 | if len(args) == 1: 336 | return args[0] 337 | return args 338 | 339 | @staticmethod 340 | def perf_counter(): 341 | return time.perf_counter() if _Peek._fixed_perf_counter is None else _Peek._fixed_perf_counter 342 | 343 | def __init__(self, parent=None, **kwargs): 344 | self._attributes = _Peek.spec_to_attributes(**kwargs) 345 | self._parent = parent 346 | 347 | def new(self, ignore_toml=False, **kwargs): 348 | if ignore_toml: 349 | return _Peek(**kwargs, parent=_peek_no_toml) 350 | else: 351 | return _Peek(**kwargs, parent=_peek_toml) 352 | 353 | def fork(self, **kwargs): 354 | return _Peek(**kwargs, parent=self) 355 | 356 | def clone(self, **kwargs): 357 | clone = _Peek(parent=self._parent) 358 | clone._attributes = self._attributes | _Peek.spec_to_attributes(**kwargs) 359 | return clone 360 | 361 | def configure(self, **kwargs): 362 | self._attributes.update(_Peek.spec_to_attributes(**kwargs)) 363 | 364 | def __getattr__(self, item): 365 | item = _Peek.de_alias(item) 366 | if item in _Peek.name_default or item == "delta1": 367 | node = self 368 | while item not in node._attributes or node._attributes[item] is None: 369 | node = node._parent 370 | if item == "delta": 371 | return _Peek.perf_counter() - node._attributes["delta1"] + node._attributes["delta"] 372 | elif item == "delta1": 373 | return node._attributes["delta"] 374 | elif item == "prefix": 375 | prefix = node._attributes[item] 376 | return str(prefix() if callable(prefix) else prefix) 377 | else: 378 | return node._attributes[item] 379 | else: 380 | return self.__getattribute__(item) 381 | 382 | def __setattr__(self, item, value): 383 | if item in ("_parent", "_is_context_manager", "_line_number_with_filename_and_parent", "_save_traceback", "_enter_time", "_as_str", "_attributes"): 384 | return super().__setattr__(item, value) 385 | self._attributes.update(_Peek.spec_to_attributes(**{item: value})) 386 | 387 | def __repr__(self): 388 | pairs = [ 389 | str(name) + "=" + repr(getattr(self, "delta1") if name == "delta" else getattr(self, name)) for name in _Peek.name_default if name != "serialize" 390 | ] 391 | return "peek.new(" + ", ".join(pairs) + ")" 392 | 393 | def __str__(self): 394 | pairs = [ 395 | str(name) + "=" + repr(getattr(self, "delta1") if name == "delta" else getattr(self, name)) for name in _Peek.name_default if name != "serialize" 396 | ] 397 | 398 | return "peek with attributes:\n " + "\n ".join(pairs) + ")" 399 | 400 | def fix_perf_counter(self, val): # for tests 401 | _Peek._fixed_perf_counter = val 402 | 403 | def do_show(self): 404 | if self.filter.strip() != "": 405 | if not eval(self.filter, {name: getattr(self, name) for name in list(_Peek.name_default) + list(_Peek.alias_default)}): 406 | return False 407 | return self.enabled 408 | 409 | def print(self, *args, as_str=False, **kwargs): 410 | if "print" in kwargs and "print_like" in kwargs: 411 | raise AttributeError("both print_like and print specified") 412 | if "print" not in kwargs and "print_like" not in kwargs: 413 | kwargs["print_like"] = True 414 | return self(*args, as_str=as_str, **kwargs) 415 | 416 | def __call__(self, *args, as_str=False, _via_module=False, **kwargs): 417 | def add_to_pairs(pairs, left, right): 418 | if right is locals or right is globals or right is vars: 419 | frame = inspect.currentframe().f_back.f_back 420 | if _via_module: 421 | frame = frame.f_back 422 | for name, value in {locals: frame.f_locals, globals: frame.f_globals, vars: frame.f_locals}[right].items(): 423 | if not (isinstance(value, _PeekModule) or name.startswith("__")): 424 | pairs.append(Pair(left=f"{name}{this.equals_separator}", right=value)) 425 | else: 426 | pairs.append(Pair(left=left, right=right)) 427 | 428 | any_args = bool(args) 429 | this = self.fork(**kwargs) 430 | if this.line_length in (0, "terminal_width"): 431 | this.line_length = shutil.get_terminal_size().columns 432 | 433 | this._as_str = as_str 434 | 435 | if this.print_like: 436 | seps = [name for name in ("sep", "separator") if name in kwargs] 437 | sepps = [name for name in ("sepp", "separator_print") if name in kwargs] 438 | 439 | if seps: 440 | if sepps: 441 | raise AttributeError(f"not allowed to use {seps[0]} and {sepps[0]} both") 442 | this.separator_print = this.separator 443 | 444 | this.values_only = True 445 | this.show_traceback = False 446 | this.to_clipboard = False 447 | this.return_none = True 448 | this.quote_string = False 449 | args = [this.separator_print.join(map(str, args))] 450 | 451 | if len(args) != 0 and not this.do_show(): 452 | # if there are no args, the checks for decorator and context manager always to be done 453 | if as_str: 454 | return "" 455 | else: 456 | return _Peek.return_args(args, this.return_none) 457 | 458 | self._is_context_manager = False 459 | 460 | Pair = collections.namedtuple("Pair", "left right") 461 | 462 | call_frame = inspect.currentframe() 463 | filename0 = call_frame.f_code.co_filename 464 | 465 | call_frame = call_frame.f_back 466 | 467 | filename = call_frame.f_code.co_filename 468 | 469 | if filename == filename0: 470 | call_frame = call_frame.f_back 471 | filename = call_frame.f_code.co_filename 472 | 473 | if filename in ("", ""): 474 | filename_name = "" 475 | code = "\n\n" 476 | this_line = "" 477 | this_line_prev = "" 478 | line_number = 0 479 | parent_function = "" 480 | else: 481 | try: 482 | main_file = sys.modules["__main__"].__file__ 483 | main_file_resolved = os.path.abspath(main_file) 484 | except AttributeError: 485 | main_file_resolved = None 486 | filename_resolved = os.path.abspath(filename) 487 | if (filename.startswith("<") and filename.endswith(">")) or (main_file_resolved is None) or (filename_resolved == main_file_resolved): 488 | filename_name = "" 489 | else: 490 | filename_name = "[" + os.path.basename(filename) + "]" 491 | 492 | if filename not in _Peek.codes: 493 | frame_info = inspect.getframeinfo(call_frame, context=1000000) # get the full source code 494 | if frame_info.code_context is None: 495 | _Peek.code[filename] = "" 496 | else: 497 | _Peek.codes[filename] = frame_info.code_context 498 | 499 | code = _Peek.codes[filename] 500 | frame_info = inspect.getframeinfo(call_frame, context=1) 501 | if frame_info.code_context is None: 502 | line_number = 0 503 | else: 504 | line_number = frame_info.lineno 505 | 506 | parent_function = executing.Source.executing(call_frame).code_qualname() 507 | parent_function = parent_function.replace("..", ".") 508 | if parent_function == "" or str(this.show_line_number) in ("n", "no parent"): 509 | parent_function = "" 510 | else: 511 | parent_function = f" in {parent_function}()" 512 | try: 513 | this_line = code[line_number - 1].strip() 514 | except IndexError: 515 | this_line = "" 516 | try: 517 | this_line_prev = code[line_number - 2].strip() 518 | except IndexError: 519 | this_line_prev = "" 520 | if this_line.startswith("@") or this_line_prev.startswith("@"): 521 | if as_str: 522 | raise TypeError("as_str may not be True when peek used as decorator") 523 | if any_args: 524 | raise TypeError("non-keyword arguments are not allowed when peek used as decorator") 525 | 526 | for ln, line in enumerate(code[line_number - 1 :], line_number): 527 | if line.strip().startswith("def") or line.strip().startswith("class"): 528 | line_number = ln 529 | break 530 | else: 531 | line_number += 1 532 | this._line_number_with_filename_and_parent = f"#{line_number}{filename_name}{parent_function}" 533 | 534 | def real_decorator(function): 535 | @functools.wraps(function) 536 | def wrapper(*args, **kwargs): 537 | enter_time = _Peek.perf_counter() 538 | context = this.context() 539 | 540 | args_kwargs = [repr(arg) for arg in args] + [f"{k}={repr(v)}" for k, v in kwargs.items()] 541 | function_arguments = function.__name__ + "(" + (", ".join(args_kwargs)) + ")" 542 | 543 | if this.show_enter: 544 | this.do_output(f"{context}called {function_arguments}{this.traceback()}") 545 | result = function(*args, **kwargs) 546 | duration = _Peek.perf_counter() - enter_time 547 | 548 | context = this.context() 549 | if this.show_exit: 550 | this.do_output(f"{context}returned {repr(result)} from {function_arguments} in {duration:.6f} seconds{this.traceback()}") 551 | 552 | return result 553 | 554 | return wrapper 555 | 556 | if not this.do_show() or (not this.show_enter and not this.show_exit): 557 | return lambda x: x 558 | 559 | return real_decorator 560 | 561 | call_node = executing.Source.executing(call_frame).node 562 | if call_node is None: 563 | this._line_number_with_filename_and_parent = "" 564 | else: 565 | line_number = call_node.lineno 566 | try: 567 | this_line = code[line_number - 1].strip() 568 | except IndexError: 569 | this_line = "" 570 | this._line_number_with_filename_and_parent = f"#{line_number}{filename_name}{parent_function}" 571 | 572 | if this_line.startswith("with ") or this_line.startswith("with\t"): 573 | if as_str: 574 | raise TypeError("as_str may not be True when peek used as context manager") 575 | if any_args: 576 | raise TypeError("non-keyword arguments are not allowed when peek used as context manager") 577 | 578 | this._is_context_manager = True 579 | return this 580 | 581 | if not this.do_show(): 582 | if as_str: 583 | return "" 584 | else: 585 | return _Peek.return_args(args, this.return_none) 586 | 587 | out = "" 588 | 589 | if args: 590 | context = this.context() 591 | pairs = [] 592 | if call_node is None or this.values_only: 593 | for right in args: 594 | add_to_pairs(pairs, "", right) 595 | else: 596 | source = executing.Source.for_frame(call_frame) 597 | for node, right in zip(call_node.args, args): 598 | left = source.asttokens().get_text(node) 599 | if "\n" in left: 600 | left = " " * node.first_token.start[1] + left 601 | left = textwrap.dedent(left) 602 | try: 603 | ast.literal_eval(left) # it's indeed a literal 604 | left = "" 605 | except Exception: 606 | pass 607 | if left: 608 | try: 609 | if isinstance(left, str): 610 | s = ast.parse(left, mode="eval") 611 | if isinstance(s, ast.Expression): 612 | s = s.body 613 | if s and isinstance(s, ast.JoinedStr): # it is indeed an f-string 614 | if this.values_only_for_fstrings: 615 | left = "" 616 | except Exception: 617 | pass 618 | if left: 619 | left += this.equals_separator 620 | add_to_pairs(pairs, left, right) 621 | 622 | just_one_line = False 623 | 624 | if not (len(pairs) > 1 and this.separator == ""): 625 | if not any("\n" in pair.left for pair in pairs): 626 | as_one_line = context + this.separator.join(pair.left + this.serialize_kwargs(obj=pair.right, width=10000) for pair in pairs) 627 | # as_one_line = context + this.separator.join(pair.left + (this.serialize_kwargs(obj=pair.right, width=10000)) for pair in pairs) 628 | if len(as_one_line) <= this.line_length and "\n" not in as_one_line: 629 | out += as_one_line 630 | just_one_line = True 631 | 632 | if not just_one_line: 633 | if isinstance(this.wrap_indent, numbers.Number): 634 | wrap_indent = int(this.wrap_indent) * " " 635 | else: 636 | wrap_indent = str(this.wrap_indent) 637 | 638 | if context.strip(): 639 | if len(context.rstrip()) >= len(wrap_indent): 640 | indent1 = wrap_indent 641 | indent1_rest = wrap_indent 642 | lines = [context] 643 | else: 644 | indent1 = context.rstrip().ljust(len(wrap_indent)) 645 | indent1_rest = wrap_indent 646 | lines = [] 647 | else: 648 | indent1 = "" 649 | indent1_rest = "" 650 | lines = [] 651 | 652 | for pair in pairs: 653 | do_right = False 654 | if "\n" in pair.left: 655 | for s in pair.left.splitlines(): 656 | lines.append(indent1 + s) 657 | do_right = True 658 | else: 659 | start = indent1 + pair.left 660 | line = start + this.serialize_kwargs(obj=pair.right, width=this.line_length - len(start)) 661 | if "\n" in line: 662 | lines.append(start) 663 | do_right = True 664 | else: 665 | lines.append(line) 666 | indent1 = indent1_rest 667 | if do_right: 668 | indent2 = indent1 + wrap_indent 669 | line = this.serialize_kwargs(obj=pair.right, width=this.line_length - len(indent2)) 670 | for s in line.splitlines(): 671 | lines.append(indent2 + s) 672 | if len(lines) > this.max_lines: 673 | lines = lines[: this.max_lines] + ["[abbreviated]"] 674 | out += "\n".join(line.rstrip() for line in lines) 675 | 676 | else: 677 | if not this.show_line_number: # if "n" or "no parent", keep that info 678 | this.show_line_number = True 679 | 680 | out += this.context(omit_context_separator=True) 681 | 682 | if this.show_traceback: 683 | out += this.traceback() 684 | 685 | if as_str: 686 | if this.do_show(): 687 | if this.use_color and this.color not in ("", "-"): 688 | out = f"{_Peek._color_name_to_ANSI[this.color.lower()]}{out}{_Peek._color_name_to_ANSI['-']}" 689 | if this.end == "\n": 690 | out += this.end 691 | else: 692 | out += f"{_Peek._color_name_to_ANSI[this.color.lower()]}{this.end}{_Peek._color_name_to_ANSI['-']}" 693 | else: 694 | out += this.end 695 | return out 696 | else: 697 | return "" 698 | 699 | if this.to_clipboard: 700 | peek.copy_to_clipboard(pairs[-1].right if "pairs" in locals() else "", confirm=False) 701 | this.do_output(out) 702 | 703 | return _Peek.return_args(args, this.return_none) 704 | 705 | @contextlib.contextmanager 706 | def preserve(self): 707 | save = dict(self._attributes) 708 | yield 709 | self._attributes = save 710 | 711 | def __enter__(self): 712 | if not hasattr(self, "_is_context_manager"): 713 | raise ValueError("not allowed as context_manager") 714 | self._save_traceback = self.traceback() 715 | self._enter_time = _Peek.perf_counter() 716 | if self.show_enter: 717 | context = self.context() 718 | self.do_output(context + "enter" + self._save_traceback) 719 | return self 720 | 721 | def __exit__(self, *args): 722 | if self.show_exit: 723 | context = self.context() 724 | duration = _Peek.perf_counter() - self._enter_time 725 | self.do_output(f"{context}exit in {duration:.6f} seconds{self._save_traceback}") 726 | self._is_context_manager = False 727 | 728 | def context(self, omit_line_number=False, omit_context_separator=False): 729 | if not omit_line_number and self.show_line_number and self._line_number_with_filename_and_parent != "": 730 | parts = [self._line_number_with_filename_and_parent] 731 | else: 732 | parts = [] 733 | if self.show_time: 734 | parts.append("@ " + str(datetime.datetime.now().strftime("%H:%M:%S.%f"))) 735 | 736 | if self.show_delta: 737 | parts.append(f"delta={self.delta:.3f}") 738 | 739 | context = " ".join(parts) 740 | if not omit_context_separator and context: 741 | context += self.context_separator 742 | 743 | return self.prefix + context 744 | 745 | def add_color_value(self, s): 746 | if not self.use_color: 747 | return s 748 | if self.color_value == "": 749 | ... 750 | elif self.color_value == "-": 751 | if self.color not in ("", "-"): 752 | s = _Peek._color_name_to_ANSI["-"] + s + _Peek._color_name_to_ANSI[self.color] 753 | else: 754 | if self.color_value != self.color: 755 | s = _Peek._color_name_to_ANSI[self.color_value] + s + _Peek._color_name_to_ANSI[self.color] 756 | return s 757 | 758 | def do_output(self, s): 759 | if self.do_show(): 760 | if self.use_color and self.color not in ("", "-"): 761 | s_end = f"{_Peek._color_name_to_ANSI[self.color.lower()]}{s}{_Peek._color_name_to_ANSI['-']}" 762 | if self.end == "\n": 763 | s_end += "\n" 764 | else: 765 | s_end += f"{_Peek._color_name_to_ANSI[self.color.lower()]}{self.end}{_Peek._color_name_to_ANSI['-']}" 766 | else: 767 | s_end = f"{s}{self.end}" 768 | 769 | if callable(self.output): 770 | if self.output == builtins.print or "end" in inspect.signature(self.output).parameters: 771 | # test for builtins.print is required as for Python <= 3.10, builtins.print has no signature 772 | self.output(s_end, end="") 773 | else: 774 | self.output(s_end) 775 | elif self.output in ("stdout", "stderr"): 776 | file = sys.stdout if self.output == "stdout" else sys.stderr 777 | if Pythonista: 778 | _Peek.print_pythonista_color(s_end, end="", file=file) 779 | # elif Pyodide: # not handled via use_color 780 | # _Peek.print_without_color(s, end=self.end, file=file) 781 | else: 782 | print(s_end, end="", file=file) 783 | elif self.output == "logging.debug": 784 | logging.debug(s) 785 | elif self.output == "logging.info": 786 | logging.info(s) 787 | elif self.output == "logging.warning": 788 | logging.warning(s) 789 | elif self.output == "logging.error": 790 | logging.error(s) 791 | elif self.output == "logging.critical": 792 | logging.critical(s) 793 | elif self.output in ("", "null"): 794 | pass 795 | elif isinstance(self.output, str): 796 | with open(self.output, "a+", encoding="utf-8") as f: 797 | print(s_end, file=f, end="") 798 | elif isinstance(self.output, Path): 799 | with self.output.open("a+", encoding="utf-8") as f: 800 | print(s_end, file=f, end="") 801 | else: 802 | print(s_end, file=self.output, end="") 803 | 804 | def copy_to_clipboard(self, value, confirm=True): 805 | if Pythonista: 806 | import clipboard 807 | 808 | clipboard.set(str(value)) 809 | else: 810 | try: 811 | import pyperclip 812 | except ImportError: 813 | raise ImportError("pyperclip not installed. Use pip install pyperclip to make it work.") 814 | pyperclip.copy(str(value)) 815 | if confirm: 816 | print(f"copied to clipboard: {value}") 817 | 818 | @property 819 | def stop(self): 820 | if self.enabled: 821 | if Pyodide: 822 | raise Exception("stopped by peek.stop") 823 | elif Pythonista: 824 | print("stopped by peek.stop") 825 | sys.exit() 826 | else: 827 | raise SystemExit("stopped by peek.stop") 828 | 829 | def traceback(self): 830 | if self.show_traceback: 831 | if isinstance(self.wrap_indent, numbers.Number): 832 | wrap_indent = int(self.wrap_indent) * " " 833 | else: 834 | wrap_indent = str(self.wrap_indent) 835 | 836 | result = "\n" + wrap_indent + "Traceback (most recent call last)\n" 837 | # Python 2.7 does not allow entry.filename, entry.line, etc, so we have to index entry 838 | return result + "\n".join( 839 | wrap_indent + ' File "' + entry[0] + '", line ' + str(entry[1]) + ", in " + entry[2] + "\n" + wrap_indent + " " + entry[3] 840 | for entry in traceback.extract_stack()[:-2] 841 | ) 842 | else: 843 | return "" 844 | 845 | def serialize_kwargs(self, obj, width): 846 | if self.format: 847 | if isinstance(self.format, str): 848 | iterator = iter([self.format]) 849 | else: 850 | iterator = iter(self.format) 851 | for sub_format in iterator: 852 | format_string = "{" + sub_format + "}" if sub_format.startswith(":") or sub_format.startswith("!") else "{:" + sub_format + "}" 853 | try: 854 | return format_string.format(obj) 855 | except Exception: 856 | ... 857 | if isinstance(obj, str): 858 | if not self.quote_string: 859 | return str(self.add_color_value(obj)) 860 | kwargs = { 861 | key: getattr(self, key) 862 | for key in ("sort_dicts", "compact", "indent", "depth", "underscore_numbers") 863 | if key in inspect.signature(self.serialize).parameters 864 | } 865 | if "width" in inspect.signature(self.serialize).parameters: 866 | kwargs["width"] = width 867 | return self.add_color_value(self.serialize(obj, **kwargs).replace("\\n", "\n")) 868 | 869 | def reset(self): 870 | reset() 871 | 872 | 873 | def reset(): 874 | global _peek_no_toml 875 | global _peek_toml 876 | global peek 877 | 878 | _Peek.in_read_toml_message = "" 879 | _peek_no_toml = _Peek(**_Peek.name_default) 880 | _peek_toml = _Peek(**(_Peek.name_default | _Peek.read_toml())) 881 | _Peek.in_read_toml_message = "" 882 | peek = _peek_toml.new() 883 | builtins.peek = peek 884 | 885 | 886 | class _PeekModule(types.ModuleType): 887 | def __call__(self, *args, **kwargs): 888 | return peek(*args, **kwargs, _via_module=True) 889 | 890 | def __setattr__(self, item, value): 891 | setattr(peek, item, value) 892 | 893 | def __getattr__(self, item): 894 | return getattr(peek, item) 895 | 896 | def __repr__(self): 897 | return repr(peek) 898 | 899 | def __str__(self): 900 | return str(peek) 901 | 902 | 903 | reset() 904 | 905 | if __name__ != "__main__": 906 | sys.modules["peek"].__class__ = _PeekModule 907 | -------------------------------------------------------------------------------- /tests/test_peek.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import division 3 | 4 | import sys 5 | import datetime 6 | import time 7 | import pytest 8 | import os 9 | import shutil 10 | from pathlib import Path 11 | 12 | 13 | import os, sys # three lines to use the local package and chdir 14 | os.chdir(os.path.dirname(__file__)) 15 | sys.path.insert(0, os.path.dirname(__file__) + "/../") 16 | 17 | import peek 18 | 19 | peek = peek.new(ignore_toml=True) 20 | 21 | dark_black=peek.ANSI.dark_black 22 | dark_red=peek.ANSI.dark_red 23 | dark_green=peek.ANSI.dark_green 24 | dark_yellow=peek.ANSI.dark_yellow 25 | dark_blue=peek.ANSI.dark_blue 26 | dark_magenta=peek.ANSI.dark_magenta 27 | dark_cyan=peek.ANSI.dark_cyan 28 | dark_white=peek.ANSI.dark_white 29 | black=peek.ANSI.black 30 | red=peek.ANSI.red 31 | green=peek.ANSI.green 32 | yellow=peek.ANSI.yellow 33 | blue=peek.ANSI.blue 34 | magenta=peek.ANSI.magenta 35 | cyan=peek.ANSI.cyan 36 | white=peek.ANSI.white 37 | reset=peek.ANSI.reset 38 | 39 | Pythonista = sys.platform == "ios" 40 | 41 | FAKE_TIME = datetime.datetime(2021, 1, 1, 0, 0, 0) 42 | 43 | 44 | @pytest.fixture 45 | def patch_datetime_now(monkeypatch): 46 | class mydatetime: 47 | @classmethod 48 | def now(cls): 49 | return FAKE_TIME 50 | 51 | monkeypatch.setattr(datetime, "datetime", mydatetime) 52 | 53 | 54 | def test_time(patch_datetime_now): 55 | hello = "world" 56 | s = peek(hello, show_time=True, as_str=True) 57 | assert s == "@ 00:00:00.000000 ==> hello='world'\n" 58 | 59 | 60 | def test_no_arguments(capsys): 61 | result = peek() 62 | out, err = capsys.readouterr() 63 | assert out.startswith("#") 64 | assert out.endswith(" in test_no_arguments()\n") 65 | assert result is None 66 | 67 | 68 | def test_one_arguments(capsys): 69 | hello = "world" 70 | result = peek(hello) 71 | peek(hello) 72 | out, err = capsys.readouterr() 73 | assert ( 74 | out 75 | == """\ 76 | hello='world' 77 | hello='world' 78 | """ 79 | ) 80 | assert result == hello 81 | 82 | 83 | def test_illegal_assignment(): 84 | with pytest.raises(AttributeError): 85 | peek.colour = "red" 86 | 87 | 88 | def test_two_arguments(capsys): 89 | hello = "world" 90 | ll = [1, 2, 3] 91 | result = peek(hello, ll) 92 | out, err = capsys.readouterr() 93 | assert out == "hello='world', ll=[1, 2, 3]\n" 94 | assert result == (hello, ll) 95 | 96 | 97 | def test_in_function(capsys): 98 | def hello(val): 99 | peek(val, show_line_number=True) 100 | 101 | hello("world") 102 | out, err = capsys.readouterr() 103 | assert out.startswith("#") 104 | assert out.endswith(" in test_in_function.hello() ==> val='world'\n") 105 | 106 | 107 | def test_in_function_no_parent(capsys): 108 | def hello(val): 109 | peek(val, show_line_number="n") 110 | 111 | hello("world") 112 | out, err = capsys.readouterr() 113 | assert out.startswith("#") 114 | assert not out.endswith(" in test_in_function_no_parent.hello() ==> val='world'\n") 115 | 116 | 117 | def test_prefix(capsys): 118 | hello = "world" 119 | peek(hello, prefix="==> ") 120 | out, err = capsys.readouterr() 121 | assert out == "==> hello='world'\n" 122 | 123 | 124 | def test_time_delta(): 125 | sdelta0 = peek(1, show_delta=True, as_str=True) 126 | stime0 = peek(1, show_time=True, as_str=True) 127 | time.sleep(0.001) 128 | sdelta1 = peek(1, show_delta=True, as_str=True) 129 | stime1 = peek(1, show_time=True, as_str=True) 130 | assert sdelta0 != sdelta1 131 | assert stime0 != stime1 132 | peek.delta = 10 133 | time.sleep(0.1) 134 | assert 10.05 < peek.delta < 11 135 | 136 | 137 | def test_dynamic_prefix(capsys): 138 | i = 0 139 | 140 | def prefix(): 141 | nonlocal i 142 | i += 1 143 | return str(i) + ")" 144 | 145 | hello = "world" 146 | peek(hello, prefix=prefix) 147 | peek(hello, prefix=prefix) 148 | out, err = capsys.readouterr() 149 | assert out == "1)hello='world'\n2)hello='world'\n" 150 | 151 | 152 | def test_values_only(): 153 | with peek.preserve(): 154 | peek.configure(values_only=True) 155 | hello = "world" 156 | s = peek(hello, as_str=True) 157 | assert s == "'world'\n" 158 | 159 | 160 | def test_locals(capsys): 161 | def square(x): 162 | result = x**2 163 | peek(locals) 164 | 165 | square(10) 166 | out, err = capsys.readouterr() 167 | assert out == "x=10, result=100\n" 168 | 169 | 170 | def test_globals(capsys): 171 | def square(x): 172 | result = x**2 173 | peek(globals) 174 | 175 | square(10) 176 | out, err = capsys.readouterr() 177 | assert "pytest== (3, 8): 377 | assert s0 == "world={'EN': 'world', 'NL': 'wereld', 'FR': 'monde', 'DE': 'Welt'}\n" 378 | assert s1 == "world={'EN': 'world', 'NL': 'wereld', 'FR': 'monde', 'DE': 'Welt'}\n" 379 | assert s2 == "world={'DE': 'Welt', 'EN': 'world', 'FR': 'monde', 'NL': 'wereld'}\n" 380 | else: 381 | assert s0 == s1 == s2 == "world={'DE': 'Welt', 'EN': 'world', 'FR': 'monde', 'NL': 'wereld'}\n" 382 | 383 | 384 | def test_underscore_numbers(): 385 | numbers = dict(x1=1, x2=1000, x3=1000000, x4=1234567890) 386 | s0 = peek(numbers, as_str=True) 387 | s1 = peek(numbers, underscore_numbers=True, as_str=True) 388 | s2 = peek(numbers, un=False, as_str=True) 389 | 390 | if sys.version_info >= (3, 10): 391 | assert s0 == s2 == "numbers={'x1': 1, 'x2': 1000, 'x3': 1000000, 'x4': 1234567890}\n" 392 | assert s1 == "numbers={'x1': 1, 'x2': 1_000, 'x3': 1_000_000, 'x4': 1_234_567_890}\n" 393 | else: 394 | assert s0 == s1 == s2 == "numbers={'x1': 1, 'x2': 1000, 'x3': 1000000, 'x4': 1234567890}\n" 395 | 396 | 397 | @pytest.mark.skipif(sys.version_info[:2] == (3, 10), reason="version 3.10 problem") 398 | def test_multiline(): 399 | a = 1 400 | b = 2 401 | ll = list(range(15)) 402 | # fmt: off 403 | s = peek((a, b), 404 | [ll, 405 | ll], as_str=True, line_length=80) 406 | # fmt: on 407 | assert ( 408 | s 409 | == """\ 410 | (a, b)=(1, 2) 411 | [ll, 412 | ll]= 413 | [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 414 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]] 415 | """ 416 | ) 417 | 418 | lines = "\n".join("line{i}".format(i=i) for i in range(4)) 419 | result = peek(lines, as_str=True) 420 | assert ( 421 | result 422 | == """\ 423 | lines= 424 | 'line0 425 | line1 426 | line2 427 | line3' 428 | """ 429 | ) 430 | 431 | 432 | @pytest.mark.skipif(Pythonista, reason="Pythonista does not use escape sequences for color") 433 | def test_filter(capsys): 434 | def gen(): 435 | for level, color in enumerate("- blue red green blue".split()): 436 | peek(level, level=level, color=color) 437 | 438 | peek.filter = "True" 439 | gen() 440 | peek.filter = "level >=2" 441 | gen() 442 | peek.filter = "level >=2" 443 | gen() 444 | peek.filter = "level >=2 and color=='blue'" 445 | gen() 446 | peek.filter = "" 447 | gen() 448 | 449 | out, err = capsys.readouterr() 450 | assert ( 451 | out 452 | == f"""\ 453 | level=0 454 | {blue}level=1{reset} 455 | {red}level=2{reset} 456 | {green}level=3{reset} 457 | {blue}level=4{reset} 458 | {red}level=2{reset} 459 | {green}level=3{reset} 460 | {blue}level=4{reset} 461 | {red}level=2{reset} 462 | {green}level=3{reset} 463 | {blue}level=4{reset} 464 | {blue}level=4{reset} 465 | level=0 466 | {blue}level=1{reset} 467 | {red}level=2{reset} 468 | {green}level=3{reset} 469 | {blue}level=4{reset} 470 | """ 471 | ) 472 | 473 | 474 | def test_color(capsys): 475 | with peek.preserve(): 476 | hello = "world" 477 | s = peek(hello, as_str=True) 478 | assert s == f"hello='world'\n" 479 | s = peek(hello, as_str=True, color="") 480 | assert s == f"hello='world'\n" 481 | s = peek(hello, as_str=True, color="-") 482 | assert s == f"hello='world'\n" 483 | s = peek(hello, as_str=True, color="red") 484 | assert s == f"{red}hello='world'{reset}\n" 485 | 486 | peek.color_value = "-" 487 | s = peek(hello, as_str=True) 488 | assert s == f"hello='world'\n" 489 | s = peek(hello, as_str=True, color="") 490 | assert s == f"hello='world'\n" 491 | s = peek(hello, as_str=True, color="-") 492 | assert s == f"hello='world'\n" 493 | s = peek(hello, as_str=True, color="red") 494 | assert s == f"{red}hello={reset}'world'{red}{reset}\n" 495 | 496 | peek.color_value = "blue" 497 | s = peek(hello, as_str=True) 498 | assert s == f"hello={blue}'world'{reset}\n" 499 | s = peek(hello, as_str=True, color="") 500 | assert s == f"hello={blue}'world'{reset}\n" 501 | s = peek(hello, as_str=True, color="-") 502 | assert s == f"hello={blue}'world'{reset}\n" 503 | s = peek(hello, as_str=True, color="red") 504 | assert s == f"{red}hello={blue}'world'{red}{reset}\n" 505 | s = peek(hello, as_str=True, color="blue") 506 | assert s == f"{blue}hello='world'{reset}\n" 507 | 508 | def test_numeric_colors(): 509 | with peek.preserve(): 510 | hello = "world" 511 | s = peek(hello, as_str=True) 512 | assert s == f"hello='world'\n" 513 | s = peek(hello, as_str=True, color=3) 514 | assert s == f"{red}hello='world'{reset}\n" 515 | 516 | peek.color_value = 0 517 | s = peek(hello, as_str=True) 518 | assert s == f"hello='world'\n" 519 | s = peek(hello, as_str=True, color=0) 520 | assert s == f"hello='world'\n" 521 | s = peek(hello, as_str=True, color=3) 522 | assert s == f"{red}hello={reset}'world'{red}{reset}\n" 523 | 524 | peek.color_value = 4 525 | s = peek(hello, as_str=True) 526 | assert s == f"hello={blue}'world'{reset}\n" 527 | s = peek(hello, as_str=True, color=0) 528 | assert s == f"hello={blue}'world'{reset}\n" 529 | s = peek(hello, as_str=True, color=3) 530 | assert s == f"{red}hello={blue}'world'{red}{reset}\n" 531 | s = peek(hello, as_str=True, color=4) 532 | assert s == f"{blue}hello='world'{reset}\n" 533 | 534 | def test_color_alias(): 535 | with peek.preserve(): 536 | peek.col="red" 537 | assert peek.color == peek.col == peek.c == "red" 538 | peek.c="green" 539 | assert peek.color == peek.col == peek.c == "green" 540 | 541 | peek.col_val="red" 542 | assert peek.color_value == peek.col_val == peek.cv == "red" 543 | peek.cv="green" 544 | assert peek.color_value == peek.col_val == peek.cv == "green" 545 | 546 | def test_incorrect_filter(): 547 | with pytest.raises(AttributeError): 548 | peek.filter = "color='blue'" 549 | 550 | with pytest.raises(AttributeError): 551 | peek.filter = "colour=='blue'" 552 | 553 | 554 | def test_decorator(capsys): 555 | peek.fix_perf_counter(0) 556 | 557 | @peek() 558 | def div(x, y): 559 | return x / y 560 | 561 | @peek(show_enter=False) 562 | def add(x, y): 563 | return x + y 564 | 565 | @peek(show_exit=False) 566 | def sub(x, y): 567 | return x - y 568 | 569 | @peek(show_enter=False, show_exit=False) 570 | def pow(x, y): 571 | return x**y 572 | 573 | assert div(10, 2) == 10 / 2 574 | assert add(2, 3) == 2 + 3 575 | assert sub(10, 2) == 10 - 2 576 | assert pow(10, 2) == 10**2 577 | out, err = capsys.readouterr() 578 | assert ( 579 | out 580 | == """\ 581 | called div(10, 2) 582 | returned 5.0 from div(10, 2) in 0.000000 seconds 583 | returned 5 from add(2, 3) in 0.000000 seconds 584 | called sub(10, 2) 585 | """ 586 | ) 587 | peek.fix_perf_counter(None) 588 | 589 | 590 | def test_decorator_edge_cases(capsys): 591 | peek.fix_perf_counter(0) 592 | 593 | @peek() 594 | def mul(x, y, factor=1): 595 | return x * y * factor 596 | 597 | assert mul(5, 6) == 30 598 | assert mul(5, 6, 10) == 300 599 | assert mul(5, 6, factor=10) == 300 600 | out, err = capsys.readouterr() 601 | assert ( 602 | out 603 | == """\ 604 | called mul(5, 6) 605 | returned 30 from mul(5, 6) in 0.000000 seconds 606 | called mul(5, 6, 10) 607 | returned 300 from mul(5, 6, 10) in 0.000000 seconds 608 | called mul(5, 6, factor=10) 609 | returned 300 from mul(5, 6, factor=10) in 0.000000 seconds 610 | """ 611 | ) 612 | peek.fix_perf_counter(None) 613 | 614 | 615 | def test_decorator_with_methods(capsys): 616 | class Number: 617 | def __init__(self, value): 618 | self.value = value 619 | 620 | @peek(show_exit=False) 621 | def __mul__(self, other): 622 | if isinstance(other, Number): 623 | return self.value * other.value 624 | else: 625 | return self.value * other 626 | 627 | def __repr__(self): 628 | return self.__class__.__name__ + "(" + str(self.value) + ")" 629 | 630 | with peek.preserve(): 631 | peek.output = "stderr" 632 | a = Number(2) 633 | b = Number(3) 634 | print(a * 2) 635 | print(a * b) 636 | out, err = capsys.readouterr() 637 | assert ( 638 | err 639 | == """\ 640 | called __mul__(Number(2), 2) 641 | called __mul__(Number(2), Number(3)) 642 | """ 643 | ) 644 | assert ( 645 | out 646 | == """4 647 | 6 648 | """ 649 | ) 650 | 651 | 652 | @pytest.mark.skipif(Pythonista, reason="Pythonista problem") 653 | def test_context_manager(capsys): 654 | peek.fix_perf_counter(0) 655 | with peek(): 656 | peek(3) 657 | out, err = capsys.readouterr() 658 | assert ( 659 | out 660 | == """\ 661 | enter 662 | 3 663 | exit in 0.000000 seconds 664 | """ 665 | ) 666 | peek.fix_perf_counter(None) 667 | 668 | 669 | def test_return_none(capsys): 670 | a = 2 671 | result = peek(a, a) 672 | assert result == (a, a) 673 | result = peek(a, a, return_none=True) 674 | assert result is None 675 | out, err = capsys.readouterr() 676 | assert ( 677 | out 678 | == """\ 679 | a=2, a=2 680 | a=2, a=2 681 | """ 682 | ) 683 | 684 | 685 | @pytest.mark.skipif(sys.version_info[0:2] == (3, 10), reason="version 3.10 problem") 686 | def test_wrapping(capsys): 687 | l0 = "".join(" {c}".format(c=c) for c in "12345678") + "\n" + "".join(".........0" for c in "12345678") 688 | 689 | print(l0) 690 | ccc = cccc = 3 * ["12345678123456789012"] 691 | ccc0 = [cccc[0] + "0"] + cccc[1:] 692 | with peek.preserve(): 693 | peek.prefix = "peek| " 694 | peek.line_length = 80 695 | peek(ccc) 696 | peek(cccc) 697 | peek(ccc0) 698 | 699 | out, err = capsys.readouterr() 700 | assert ( 701 | out 702 | == """\ 703 | 1 2 3 4 5 6 7 8 704 | .........0.........0.........0.........0.........0.........0.........0.........0 705 | peek| 706 | ccc=['12345678123456789012', '12345678123456789012', '12345678123456789012'] 707 | peek| 708 | cccc= 709 | ['12345678123456789012', '12345678123456789012', '12345678123456789012'] 710 | peek| 711 | ccc0= 712 | ['123456781234567890120', 713 | '12345678123456789012', 714 | '12345678123456789012'] 715 | """ 716 | ) 717 | a = "1234" 718 | b = bb = 9 * ["123"] 719 | print(l0) 720 | with peek.preserve(): 721 | peek.prefix = "peek| " 722 | peek.line_length = 80 723 | peek(a, b) 724 | peek(a, bb) 725 | out, err = capsys.readouterr() 726 | assert ( 727 | out 728 | == """\ 729 | 1 2 3 4 5 6 7 8 730 | .........0.........0.........0.........0.........0.........0.........0.........0 731 | peek| 732 | a='1234' 733 | b=['123', '123', '123', '123', '123', '123', '123', '123', '123'] 734 | peek| 735 | a='1234' 736 | bb=['123', '123', '123', '123', '123', '123', '123', '123', '123'] 737 | """ 738 | ) 739 | dddd = 10 * ["123"] 740 | dddd = ddddd = 10 * ["123"] 741 | e = "a\nb" 742 | print(l0) 743 | with peek.preserve(): 744 | peek.prefix = "peek| " 745 | peek.line_length = 80 746 | peek(a, dddd) 747 | peek(a, ddddd) 748 | peek(e) 749 | out, err = capsys.readouterr() 750 | assert ( 751 | out 752 | == """\ 753 | 1 2 3 4 5 6 7 8 754 | .........0.........0.........0.........0.........0.........0.........0.........0 755 | peek| 756 | a='1234' 757 | dddd=['123', '123', '123', '123', '123', '123', '123', '123', '123', '123'] 758 | peek| 759 | a='1234' 760 | ddddd=['123', '123', '123', '123', '123', '123', '123', '123', '123', '123'] 761 | peek| 762 | e= 763 | 'a 764 | b' 765 | """ 766 | ) 767 | a = aa = 2 * ["0123456789ABC"] 768 | print(l0) 769 | with peek.preserve(): 770 | peek.prefix = "peek| " 771 | peek(a, line_length=40) 772 | peek(aa, line_length=40) 773 | peek(aa, line_length=41) 774 | out, err = capsys.readouterr() 775 | assert ( 776 | out 777 | == """\ 778 | 1 2 3 4 5 6 7 8 779 | .........0.........0.........0.........0.........0.........0.........0.........0 780 | peek| 781 | a=['0123456789ABC', '0123456789ABC'] 782 | peek| 783 | aa= 784 | ['0123456789ABC', 785 | '0123456789ABC'] 786 | peek| 787 | aa=['0123456789ABC', '0123456789ABC'] 788 | """ 789 | ) 790 | 791 | 792 | def test_compact(capsys): 793 | a = 9 * ["0123456789"] 794 | peek(a, ll=80) 795 | peek(a, compact=True, ll=80) 796 | out, err = capsys.readouterr() 797 | assert ( 798 | out 799 | == """\ 800 | a= 801 | ['0123456789', 802 | '0123456789', 803 | '0123456789', 804 | '0123456789', 805 | '0123456789', 806 | '0123456789', 807 | '0123456789', 808 | '0123456789', 809 | '0123456789'] 810 | a= 811 | ['0123456789', '0123456789', '0123456789', '0123456789', '0123456789', 812 | '0123456789', '0123456789', '0123456789', '0123456789'] 813 | """ 814 | ) 815 | 816 | 817 | def test_depth_indent(capsys): 818 | s = "==============================================" 819 | a = [s + "1", [s + "2", [s + "3", [s + "4"]]], s + "1"] 820 | peek(a, indent=4, ll=80) 821 | peek(a, depth=2, indent=4, ll=80) 822 | out, err = capsys.readouterr() 823 | assert ( 824 | out 825 | == """\ 826 | a= 827 | [ '==============================================1', 828 | [ '==============================================2', 829 | [ '==============================================3', 830 | ['==============================================4']]], 831 | '==============================================1'] 832 | a= 833 | [ '==============================================1', 834 | ['==============================================2', [...]], 835 | '==============================================1'] 836 | """ 837 | ) 838 | 839 | 840 | def test_enabled(capsys): 841 | with peek.preserve(): 842 | peek("One") 843 | peek.configure(enabled=False) 844 | peek("Two") 845 | s = peek("Two", as_str=True) 846 | assert s == "" 847 | peek.configure(enabled=True) 848 | peek("Three") 849 | 850 | out, err = capsys.readouterr() 851 | assert ( 852 | out 853 | == """\ 854 | 'One' 855 | 'Three' 856 | """ 857 | ) 858 | 859 | 860 | def test_enabled2(capsys): 861 | with peek.preserve(): 862 | peek.configure(enabled=False) 863 | line0 = peek("line0") 864 | noline0 = peek(prefix="no0") 865 | pair0 = peek("p0", "p0") 866 | s0 = peek("s0", as_str=True) 867 | peek.configure(enabled=[]) 868 | line1 = peek("line1") 869 | noline1 = peek(prefix="no1") 870 | pair1 = peek("p1", "p1") 871 | s1 = peek("s1", as_str=True) 872 | peek.configure(enabled=True) 873 | line2 = peek("line2") 874 | noline2 = peek(prefix="no2") 875 | pair2 = peek("p2", "p2") 876 | s2 = peek("s2", as_str=True) 877 | out, err = capsys.readouterr() 878 | assert "line0" not in out and "p0" not in out and "no0" not in out 879 | assert "line1" not in out and "p1" not in out and "no1" not in out 880 | assert "line2" in out and "p2" in out and "no2" in out 881 | assert line0 == "line0" 882 | assert line1 == "line1" 883 | assert line2 == "line2" 884 | assert noline0 is None 885 | assert noline1 is None 886 | assert noline2 is None 887 | assert pair0 == ("p0", "p0") 888 | assert pair1 == ("p1", "p1") 889 | assert pair2 == ("p2", "p2") 890 | assert s0 == "" 891 | assert s1 == "" 892 | assert s2 == "'s2'\n" 893 | 894 | 895 | def test_wrap_indent(): 896 | s = 4 * ["*******************"] 897 | with peek.preserve(): 898 | peek.prefix = "peek| " 899 | peek.line_length = 80 900 | res = peek(s, compact=True, as_str=True) 901 | assert res.splitlines()[1].startswith(" s") 902 | res = peek(s, compact=True, as_str=True, wrap_indent="....") 903 | assert res.splitlines()[1].startswith("....s") 904 | res = peek(s, compact=True, as_str=True, wrap_indent=2) 905 | assert res.splitlines()[1].startswith(" s") 906 | 907 | 908 | def test_traceback(capsys): 909 | with peek.preserve(): 910 | peek.show_traceback = True 911 | peek() 912 | out, err = capsys.readouterr() 913 | assert out.count("traceback") == 2 914 | 915 | @peek() 916 | def p(): 917 | pass 918 | 919 | p() 920 | out, err = capsys.readouterr() 921 | assert out.count("traceback") == 2 922 | with peek(): 923 | pass 924 | out, err = capsys.readouterr() 925 | assert out.count("traceback") == 2 926 | 927 | 928 | @pytest.mark.skipif(Pythonista, reason="Pythonista problem") 929 | def test_check_output(capsys, tmpdir): 930 | with peek.preserve(): 931 | x1_file = tmpdir / "x1.py" 932 | with open(str(x1_file), "w") as f: 933 | print( 934 | """\ 935 | def check_output(): 936 | import x2 937 | 938 | peek.configure(show_line_number=True, show_exit= False,use_color=False) 939 | x2.test() 940 | peek(1) 941 | peek( 942 | 1 943 | ) 944 | with peek(prefix="==>"): 945 | peek() 946 | 947 | with peek( 948 | 949 | 950 | 951 | prefix="==>" 952 | 953 | ): 954 | peek() 955 | 956 | @peek() 957 | def x(a, b=1): 958 | pass 959 | x(2) 960 | 961 | @peek() 962 | 963 | 964 | 965 | 966 | def x( 967 | 968 | 969 | ): 970 | pass 971 | 972 | x() 973 | """, 974 | file=f, 975 | ) 976 | 977 | x2_file = tmpdir / "x2.py" 978 | with open(str(x2_file), "w") as f: 979 | print( 980 | """\ 981 | 982 | def test(): 983 | @peek() 984 | def myself(x): 985 | peek(x) 986 | return x 987 | 988 | myself(6) 989 | with peek(): 990 | pass 991 | """, 992 | file=f, 993 | ) 994 | sys.path = [str(tmpdir)] + sys.path 995 | import x1 996 | 997 | x1.check_output() 998 | sys.path.pop(0) 999 | out, err = capsys.readouterr() 1000 | assert ( 1001 | out 1002 | == """\ 1003 | #4[x2.py] in test() ==> called myself(6) 1004 | #5[x2.py] in test.myself() ==> x=6 1005 | #9[x2.py] in test() ==> enter 1006 | #6[x1.py] in check_output() ==> 1 1007 | #7[x1.py] in check_output() ==> 1 1008 | ==>#10[x1.py] in check_output() ==> enter 1009 | #11[x1.py] in check_output() 1010 | ==>#13[x1.py] in check_output() ==> enter 1011 | #20[x1.py] in check_output() 1012 | #23[x1.py] in check_output() ==> called x(2) 1013 | #32[x1.py] in check_output() ==> called x() 1014 | """ 1015 | ) 1016 | 1017 | 1018 | @pytest.mark.skipif(sys.version_info[0:2] == (3, 10), reason="version 3.10 problem") 1019 | def test_prefix_variants(capsys): 1020 | n = 1 1021 | peek.prefix = lambda: f"{n:<2d}" 1022 | peek(10 * 10) 1023 | n = 2 1024 | peek(10 * 10) 1025 | peek.prefix = 1 1026 | peek(10 * 10) 1027 | out, err = capsys.readouterr() 1028 | assert ( 1029 | out 1030 | == """\ 1031 | 1 10 * 10=100 1032 | 2 10 * 10=100 1033 | 110 * 10=100 1034 | """ 1035 | ) 1036 | peek.prefix = "" 1037 | 1038 | 1039 | def test_propagation(): 1040 | with peek.preserve(): 1041 | y0 = peek.fork() 1042 | y1 = y0.fork() 1043 | peek.prefix = "x" 1044 | y2 = peek.clone() 1045 | 1046 | assert peek.prefix == "x" 1047 | assert y0.prefix == "x" 1048 | assert y1.prefix == "x" 1049 | assert y2.prefix == "x" 1050 | 1051 | y1.prefix = "xx" 1052 | assert peek.prefix == "x" 1053 | assert y0.prefix == "x" 1054 | assert y1.prefix == "xx" 1055 | assert y2.prefix == "x" 1056 | 1057 | y1.prefix = None 1058 | assert peek.prefix == "x" 1059 | assert y0.prefix == "x" 1060 | assert y1.prefix == "x" 1061 | assert y2.prefix == "x" 1062 | 1063 | peek.prefix = None 1064 | assert peek.prefix == "" 1065 | assert y0.prefix == "" 1066 | assert y1.prefix == "" 1067 | assert y2.prefix == "x" 1068 | 1069 | 1070 | def test_delta_propagation(): 1071 | with peek.preserve(): 1072 | y_delta_start = peek.delta 1073 | y0 = peek.fork() 1074 | y1 = y0.fork() 1075 | peek.delta = 100 1076 | y2 = peek.clone() 1077 | 1078 | assert 100 < peek.delta < 110 1079 | assert 100 < y0.delta < 110 1080 | assert 100 < y1.delta < 110 1081 | assert 100 < y2.delta < 110 1082 | 1083 | y1.delta = 200 1084 | assert 100 < peek.delta < 110 1085 | assert 100 < y0.delta < 110 1086 | assert 200 < y1.delta < 210 1087 | assert 100 < y2.delta < 110 1088 | 1089 | y1.delta = None 1090 | assert 100 < peek.delta < 110 1091 | assert 100 < y0.delta < 110 1092 | assert 100 < y1.delta < 110 1093 | assert 100 < y2.delta < 110 1094 | 1095 | peek.delta = None 1096 | assert 0 < peek.delta < y_delta_start + 10 1097 | assert 0 < y0.delta < y_delta_start + 10 1098 | assert 0 < y1.delta < y_delta_start + 10 1099 | assert 100 < y2.delta < 110 1100 | 1101 | 1102 | def test_end(capsys): 1103 | a = 12 1104 | b = 4 * ["test"] 1105 | c = 1 1106 | peek(a, end=" ") 1107 | peek(b, end=" ") 1108 | peek(c) 1109 | out, err = capsys.readouterr() 1110 | assert ( 1111 | out 1112 | == """\ 1113 | a=12 b=['test', 'test', 'test', 'test'] c=1 1114 | """ 1115 | ) 1116 | 1117 | 1118 | def test_separator(capsys): 1119 | a = 12 1120 | b = 4 * ["test"] 1121 | peek(a, b) 1122 | peek(a, b, sep="") 1123 | peek(a, separator="") 1124 | out, err = capsys.readouterr() 1125 | assert ( 1126 | out 1127 | == """\ 1128 | a=12, b=['test', 'test', 'test', 'test'] 1129 | a=12 1130 | b=['test', 'test', 'test', 'test'] 1131 | a=12 1132 | """ 1133 | ) 1134 | 1135 | 1136 | def test_equals_separator(capsys): 1137 | a = 12 1138 | b = 4 * ["test"] 1139 | peek(a, b) 1140 | peek(a, b, equals_separator=" ==> ") 1141 | peek(a, b, equals_separator=" = ") 1142 | 1143 | out, err = capsys.readouterr() 1144 | assert ( 1145 | out 1146 | == """\ 1147 | a=12, b=['test', 'test', 'test', 'test'] 1148 | a ==> 12, b ==> ['test', 'test', 'test', 'test'] 1149 | a = 12, b = ['test', 'test', 'test', 'test'] 1150 | """ 1151 | ) 1152 | 1153 | 1154 | def test_context_separator(capsys): 1155 | a = 12 1156 | b = 2 * ["test"] 1157 | peek(a, b, show_line_number=True) 1158 | peek(a, b, sln=1, context_separator=" ... ") 1159 | 1160 | out, err = capsys.readouterr() 1161 | lines = out.split("\n") 1162 | assert lines[0].endswith(" ==> a=12, b=['test', 'test']") 1163 | assert lines[1].endswith(" ... a=12, b=['test', 'test']") 1164 | 1165 | 1166 | def test_wrap_indent1(capsys): 1167 | with peek.preserve(): 1168 | peek.separator = "" 1169 | peek(1, 2) 1170 | peek(1, 2, prefix="p| ") 1171 | peek(1, 2, prefix="my") 1172 | peek.wrap_indent = "...." 1173 | peek(1, 2, prefix="p| ") 1174 | peek(1, 2, prefix="my") 1175 | out, err = capsys.readouterr() 1176 | assert ( 1177 | out 1178 | == """\ 1179 | 1 1180 | 2 1181 | p| 1 1182 | 2 1183 | my 1 1184 | 2 1185 | p| 1 1186 | ....2 1187 | my 1 1188 | ....2 1189 | """ 1190 | ) 1191 | 1192 | 1193 | def test_fstrings(capsys): 1194 | hello = "world" 1195 | 1196 | with peek.preserve(): 1197 | peek("hello, world") 1198 | peek(hello) 1199 | peek(f"hello={hello}") 1200 | 1201 | with peek.preserve(): 1202 | peek.values_only = True 1203 | peek("hello, world") 1204 | peek(hello) 1205 | peek(f"hello={hello}") 1206 | 1207 | with peek.preserve(): 1208 | peek.values_only_for_fstrings = True 1209 | peek("hello, world") 1210 | peek(hello) 1211 | peek(f"hello={hello}") 1212 | 1213 | with peek.preserve(): 1214 | peek.voff = True 1215 | peek.vo = True 1216 | peek("hello, world") 1217 | peek(hello) 1218 | peek(f"hello={hello}") 1219 | 1220 | out, err = capsys.readouterr() 1221 | assert ( 1222 | out 1223 | == """\ 1224 | 'hello, world' 1225 | hello='world' 1226 | f"hello={hello}"='hello=world' 1227 | 'hello, world' 1228 | 'world' 1229 | 'hello=world' 1230 | 'hello, world' 1231 | hello='world' 1232 | 'hello=world' 1233 | 'hello, world' 1234 | 'world' 1235 | 'hello=world' 1236 | """ 1237 | ) 1238 | 1239 | 1240 | @pytest.mark.skipif(Pythonista, reason="Pythonista problem") 1241 | def test_stop(): 1242 | with pytest.raises(SystemExit): 1243 | peek.stop() 1244 | 1245 | with pytest.raises(SystemExit): 1246 | peek.stop 1247 | 1248 | peek.enabled = False 1249 | peek.stop 1250 | peek.enabled = True 1251 | 1252 | 1253 | @pytest.mark.skipif(Pythonista, reason="Pythonista problem") 1254 | def test_max_lines(capsys): 1255 | a = [list(range(i, i + 10)) for i in range(10, 100, 10)] 1256 | peek(a) 1257 | out, err = capsys.readouterr() 1258 | assert ( 1259 | out 1260 | == """\ 1261 | a= 1262 | [[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 1263 | [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], 1264 | [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], 1265 | [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], 1266 | [50, 51, 52, 53, 54, 55, 56, 57, 58, 59], 1267 | [60, 61, 62, 63, 64, 65, 66, 67, 68, 69], 1268 | [70, 71, 72, 73, 74, 75, 76, 77, 78, 79], 1269 | [80, 81, 82, 83, 84, 85, 86, 87, 88, 89], 1270 | [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]] 1271 | """ 1272 | ) 1273 | peek(a, max_lines=5) 1274 | out, err = capsys.readouterr() 1275 | assert ( 1276 | out 1277 | == """\ 1278 | a= 1279 | [[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 1280 | [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], 1281 | [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], 1282 | [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], 1283 | [abbreviated] 1284 | """ 1285 | ) 1286 | 1287 | 1288 | def test_line_length(): 1289 | a = list(range(100)) 1290 | out1 = peek(a, compact=True, ll=0, as_str=True) 1291 | out2 = peek(a, compact=True, ll="terminal_width", as_str=True) 1292 | out3 = peek(a, compact=True, ll=shutil.get_terminal_size().columns, as_str=True) 1293 | assert out1 == out2 == out3 1294 | 1295 | with pytest.raises(AttributeError): 1296 | peek(1, line_length=-1) 1297 | 1298 | 1299 | if __name__ == "__main__": 1300 | pytest.main(["-vv", "-s", "-x", __file__]) 1301 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Introduction 4 | 5 | Do you debug your code with `print()` or `log()`? 6 | If so, peek will make printing debug information really easy. 7 | And on top of that, you get some basic benchmarking functionality. 8 | 9 | ## Table of contents 10 | 11 | * [Installation](#installation) 12 | 13 | * [Importing peek](#importing-peek) 14 | 15 | * [Inspect variables and expressions](#inspect-variables-and-expressions) 16 | 17 | * [Inspect execution](#inspect-execution) 18 | 19 | * [Return value](#return-value) 20 | 21 | * [Debug entry and exit of function calls](#debug-entry-and-exit-of-function-calls) 22 | 23 | * [Benchmarking with peek](#benchmarking-with-peek) 24 | 25 | * [Configuration](#configuration) 26 | 27 | * [Use peek.print to use peek like print with extras](#use-peekprint-to-use-peek-like-print-with-extras) 28 | 29 | * [Peeking locals and globals](#peeking-locals-and-globals) 30 | 31 | * [Return a string instead of sending to output](#return-a-string-instead-of-sending-to-output) 32 | 33 | * [Disabling peek's output](#disabling-peeks-output) 34 | 35 | * [Using filter to control peek output](#using-filter-to-control-peek-output) 36 | 37 | * [Copying to the clipboard](#copying-to-the-clipboard) 38 | 39 | * [Conditional stop of program](#conditional-stop-of-program) 40 | 41 | * [Interpreting the line number information](#interpreting-the-line-number-information) 42 | 43 | * [Configuring at import time](#configuring-at-import-time) 44 | 45 | * [Working with multiple instances of peek](#working-with-multiple-instances-of-peek) 46 | 47 | * [Test script](#test-script) 48 | 49 | * [Using peek in a REPL](#using-peek-in-a-repl) 50 | 51 | * [Limitations](#limitations) 52 | 53 | * [Changelog](#changelog) 54 | 55 | * [Acknowledgement](#acknowledgement) 56 | 57 | * [Differences with IceCream](#differences-with-icecream) 58 | 59 | * [Contact info](#contact-info) 60 | 61 | 62 | ## Installation 63 | 64 | Installing peek with pip is easy. 65 | ``` 66 | pip install peek-python 67 | ``` 68 | or when you want to upgrade, 69 | ``` 70 | pip install peek-python --upgrade 71 | ``` 72 | 73 | Note that peek requires the `asttokens`, `colorama`, `executing`. `six` and `tomli` modules, all of which will be automatically installed. 74 | 75 | > [!IMPORTANT] 76 | > 77 | > peek requires Python >= 3.9 78 | 79 | 80 | 81 | > [!NOTE] 82 | > 83 | > The GitHub repository can be found on https://github.com/salabim/peek . 84 | 85 | ## Importing peek 86 | 87 | All you need is: 88 | 89 | ``` 90 | import peek 91 | ``` 92 | 93 | , or the more conventional, but more verbose 94 | 95 | ``` 96 | from peek import peek 97 | ``` 98 | 99 | Note that after this, `peek` is automatically a builtin and can thus be used in any module without 100 | importing it there. 101 | 102 | 103 | ## Inspect variables and expressions 104 | 105 | Have you ever printed variables or expressions to debug your program? If you've 106 | ever typed something like 107 | 108 | ``` 109 | print(add2(1000)) 110 | ``` 111 | 112 | or the more thorough 113 | 114 | ``` 115 | print("add2(1000)", add2(1000))) 116 | ``` 117 | or: 118 | ``` 119 | print(f"{add2(1000)=}") 120 | ``` 121 | 122 | then `peek()` is here to help. With arguments, `peek()` inspects itself and prints 123 | both its own arguments and the values of those arguments. 124 | 125 | ``` 126 | def add2(i): 127 | return i + 2 128 | 129 | peek(add2(1000)) 130 | ``` 131 | 132 | prints 133 | ``` 134 | add2(1000)=1002 135 | ``` 136 | 137 | Similarly, 138 | 139 | ``` 140 | class X: 141 | a = 3 142 | world = {"EN": "world", "NL": "wereld", "FR": "monde", "DE": "Welt"} 143 | 144 | peek(world, X.a) 145 | ``` 146 | 147 | prints 148 | ``` 149 | world={"EN": "world ", "NL": "wereld", "FR": "monde", "DE": "Welt"}, X.a=3 150 | ``` 151 | Just give `peek()` a variable or expression and you're done. 152 | 153 | And you can even add color to distinguish between peek's output lines: 154 | 155 | ``` 156 | for number in range(10): 157 | number_divided_by_3 = number / 3 158 | if number % 3 == 0: 159 | peek(number, number_divided_by_3, color="red") 160 | else: 161 | peek(number, number_divided_by_3, color="yellow") 162 | ``` 163 | 164 | This will result in: 165 | 166 | 167 | 168 | ## Inspect execution 169 | 170 | Have you ever used `print()` to determine which parts of your program are 171 | executed, and in which order they're executed? For example, if you've ever added 172 | print statements to debug code like 173 | 174 | ``` 175 | def add2(i): 176 | print("***add2 1") 177 | result = i + 2 178 | print("***add2 2") 179 | return result 180 | ``` 181 | then `peek()` helps here, too. Without arguments, `peek()` inspects itself and 182 | prints the calling line number and -if applicable- the file name and parent function. 183 | 184 | ``` 185 | def add2(i): 186 | peek() 187 | result = i + 2 188 | peek() 189 | return result 190 | peek(add2(1000)) 191 | ``` 192 | 193 | prints something like 194 | ``` 195 | #3 in add2() 196 | #5 in add2() 197 | add2(1000)=1002 198 | ``` 199 | 200 | ## Return Value 201 | 202 | `peek()` returns its argument(s), so `peek()` can easily be inserted into 203 | pre-existing code. 204 | 205 | ``` 206 | def add2(i): 207 | return i + 2 208 | b = peek(add2(1000)) 209 | peek(b) 210 | ``` 211 | prints 212 | ``` 213 | add2(1000)=1002 214 | b=1002 215 | ``` 216 | ## Debug entry and exit of function calls 217 | 218 | When you apply `peek()` as a decorator to a function or method, both the entry and exit can be tracked. 219 | The (keyword) arguments passed will be shown and upon return, the return value. 220 | 221 | ``` 222 | @peek() 223 | def mul(x, y): 224 | return x * y 225 | 226 | print(mul(5, 7)) 227 | ``` 228 | prints 229 | ``` 230 | called mul(5, 7) 231 | returned 35 from mul(5, 7) in 0.000006 seconds 232 | 35 233 | ``` 234 | It is possible to suppress the print-out of either the enter or the exit information with 235 | the show_enter and show_exit parameters, like: 236 | 237 | ``` 238 | @peek(show_exit=False) 239 | def mul(x, peek): 240 | return x * peek 241 | 242 | print(mul(5, 7)) 243 | ``` 244 | prints 245 | ``` 246 | called mul(5, 7) 247 | 35 248 | ``` 249 | 250 | ## Benchmarking with peek 251 | 252 | If you decorate a function or method with peek(), you will be offered the duration between entry and exit (in seconds) as a bonus. 253 | 254 | That opens the door to simple benchmarking, like: 255 | ``` 256 | import time 257 | 258 | @peek(show_enter=False,show_line_number=True) 259 | def do_sort(i): 260 | n = 10 ** i 261 | x = sorted(list(range(n))) 262 | return f"{n:9d}" 263 | 264 | for i in range(8): 265 | do_sort(i) 266 | ``` 267 | the ouput will show the effects of the population size on the sort speed: 268 | ``` 269 | #5 ==> returned ' 1' from do_sort(0) in 0.000027 seconds 270 | #5 ==> returned ' 10' from do_sort(1) in 0.000060 seconds 271 | #5 ==> returned ' 100' from do_sort(2) in 0.000748 seconds 272 | #5 ==> returned ' 1000' from do_sort(3) in 0.001897 seconds 273 | #5 ==> returned ' 10000' from do_sort(4) in 0.002231 seconds 274 | #5 ==> returned ' 100000' from do_sort(5) in 0.024014 seconds 275 | #5 ==> returned ' 1000000' from do_sort(6) in 0.257504 seconds 276 | #5 ==> returned ' 10000000' from do_sort(7) in 1.553495 seconds 277 | ``` 278 | 279 | It is also possible to time any code by using peek() as a context manager, e.g. 280 | ``` 281 | with peek(): 282 | time.sleep(1) 283 | ``` 284 | wil print something like 285 | ``` 286 | enter 287 | exit in 1.000900 seconds 288 | ``` 289 | You can include parameters here as well: 290 | ``` 291 | with peek(show_line_number=True, show_time=True): 292 | time.sleep(1) 293 | ``` 294 | will print somethink like: 295 | ``` 296 | #8 @ 13:20:32.605903 ==> enter 297 | #8 @ 13:20:33.609519 ==> exit in 1.003358 seconds 298 | ``` 299 | 300 | Finally, to help with timing code, you can request the current delta with 301 | ``` 302 | peek.delta 303 | ``` 304 | or (re)set it with 305 | ``` 306 | peek.delta = 0 307 | ``` 308 | So, e.g. to time a section of code: 309 | ``` 310 | peek.delta = 0 311 | time.sleep(1) 312 | duration = peek.delta 313 | peek(duration) 314 | ``` 315 | might print something like: 316 | ``` 317 | duration=1.0001721999999997 318 | ``` 319 | 320 | ## Configuration 321 | 322 | For the configuration, it is important to realize that `peek` is an instance of a class, which has 323 | a number of configuration attributes: 324 | 325 | ``` 326 | ------------------------------------------------------ 327 | attribute alternative default 328 | ------------------------------------------------------ 329 | color col or c "-" 330 | color_value col_val or cv "" 331 | compact - False 332 | context_separator cs " ==> " 333 | depth - 1000000 334 | delta - 0 335 | enabled - True 336 | end - "\n" 337 | equals_separator - "=" 338 | filter f "" 339 | format fmt "" 340 | indent - 1 341 | level lvl 0 342 | line_length ll 80 343 | max_lines ml 10000000 344 | output - "stdout" 345 | prefix pr "" 346 | print_like print False 347 | quote_string qs True 348 | return_none - False 349 | separator sep ", " 350 | separator_print sepp "" " 351 | serialize - pprint.pformat 352 | show_delta sd False 353 | show_enter se True 354 | show_exit sx True 355 | show_line_number sln False 356 | show_time st False 357 | show_traceback - False 358 | sort_dicts - False 359 | to_clipboard clip False 360 | underscore_numbers *) un False 361 | use_color - True **) 362 | values_only vo False 363 | value_only_for_fstrings voff False 364 | wrap_indent - " " 365 | ------------------------------------------------------ 366 | *) ignored for Python 3.9 367 | **) False if run under pyodide 368 | ``` 369 | It is perfectly ok to set/get any of these attributes directly, like 370 | ``` 371 | peek.prefix = "==> " 372 | print(peek.prefix) 373 | ``` 374 | 375 | But, it is also possible to apply configuration directly, only here, in the call to `peek`: 376 | So, it is possible to say 377 | 378 | ``` 379 | peek(12, prefix="==> ") 380 | ``` 381 | , which will print 382 | ``` 383 | ==> 12 384 | ``` 385 | It is also possible to configure several attributes permanently with the configure method. 386 | ``` 387 | peek.configure(prefix="==> ", color="blue") 388 | peek(12) 389 | ``` 390 | will print in blue 391 | ``` 392 | ==> 12 393 | ``` 394 | It is arguably easier to say: 395 | ``` 396 | peek.prefix = "==> " 397 | peek.color = "blue" 398 | peek(12) 399 | ``` 400 | or even 401 | ``` 402 | peek.pr = "==> " 403 | peek.col = "blue" 404 | peek(12) 405 | ``` 406 | to print 407 | ``` 408 | ==> 12 409 | ``` 410 | Yet another way to configure peek is to get a new instance of peek with peek.new() and the required configuration: 411 | ``` 412 | peek0 = peek.new(prefix="==> ", color="blue") 413 | peek0(12) 414 | ``` 415 | will print 416 | ``` 417 | ==> 12 418 | ``` 419 | 420 | Or, yet another possibility is to clone peek (optionally with modified attributes): 421 | ``` 422 | peek1 = peek.clone(show_time=True) 423 | peek2 = peek.clone() 424 | peek2.show_time = True 425 | ``` 426 | After this `peek1` and `peek2` will behave similarly (but they are not the same!) 427 | 428 | ### prefix / pr 429 | 430 | ``` 431 | peek('world', prefix='hello -> ') 432 | ``` 433 | prints 434 | ``` 435 | hello -> 'world' 436 | ``` 437 | 438 | `prefix` can be a function, too. 439 | 440 | ``` 441 | import time 442 | def unix_timestamp(): 443 | return f"{int(time.time())} " 444 | hello = "world" 445 | peek.prefix = unix_timestamp 446 | peek(hello) 447 | ``` 448 | prints something like 449 | ``` 450 | 1613635601 hello='world' 451 | ``` 452 | 453 | ### output 454 | This will allow the output to be handled by something else than the default (output being written to stdout). 455 | 456 | The `output` attribute can be 457 | 458 | * a callable that accepts at least one parameter (the text to be printed) 459 | * a string or Path object that will be used as the filename 460 | * a text file that is open for writing/appending 461 | 462 | In the example below, 463 | ``` 464 | import sys 465 | peek(1, output=print) 466 | peek(2, output=sys.stderr) 467 | with open("test", "a+") as f: 468 | peek(3, output=f) 469 | peek(4, output="") 470 | ``` 471 | * `1` will be printed to stdout 472 | * `2` will be printed to stderr 473 | * `3` will be appended to the file test 474 | * nothing will be printed/written 475 | 476 | As `output` may be a callable, you can even use this to automatically log any `peek` output: 477 | ``` 478 | import logging 479 | logging.basicConfig(level="INFO") 480 | log = logging.getLogger("demo") 481 | peek.configure(output=log.info) 482 | a = {1, 2, 3, 4, 5} 483 | peek(a) 484 | a.remove(4) 485 | peek(a) 486 | ``` 487 | will print to stdout: 488 | ``` 489 | INFO:demo:a={1, 2, 3, 4, 5} 490 | INFO:demo:a={1, 2, 3, 5} 491 | ``` 492 | Finally, you can specify the following strings: 493 | ``` 494 | "stderr" to print to stderr 495 | "stdout" to print to stdout 496 | "stdout_nocolor" to print to stdout without any colors 497 | "null" or "" to completely ignore (dummy) output 498 | "logging.debug" to use logging.debug 499 | "logging.info" to use logging.info 500 | "logging.warning" to use logging.warning 501 | "logging.error" to use logging.error 502 | "logging.critical" to use logging.critical 503 | ``` 504 | E.g. 505 | ``` 506 | peek.output = "stderr" 507 | ``` 508 | to print to stderr. 509 | 510 | ### serialize 511 | This will allow to specify how argument values are to be serialized to displayable strings. 512 | The default is `pformat` (from `pprint`), but this can be changed. 513 | For example, to handle non-standard datatypes in a custom fashion. 514 | The serialize function should accept at least one parameter. 515 | The function may optionally accept the keyword arguments `width` and `sort_dicts`, `compact`, `indent`, `underscore_numbers` and `depth`. 516 | 517 | ``` 518 | def add_len(obj): 519 | if hasattr(obj, "__len__"): 520 | add = f" [len={len(obj)}]" 521 | else: 522 | add = "" 523 | return f"{repr(obj)}{add}" 524 | 525 | zero_to_six = list(range(7)) 526 | hello = "world" 527 | peek(7, hello, zero_to_six, serialize=add_len) 528 | ``` 529 | prints 530 | ``` 531 | 7, hello='world' [len=5], zero_to_six=[0, 1, 2, 3, 4, 5, 6] [len=7] 532 | ``` 533 | 534 | ### show_line_number / sln 535 | If True, adds the `peek()` call's line number and possibly the filename and parent function to `peek()`'s output. 536 | 537 | ``` 538 | peek.configure(show_line_number=True) 539 | def shout(): 540 | hello="world" 541 | peek(hello) 542 | shout() 543 | ``` 544 | prints something like 545 | ``` 546 | #5 in shout() ==> hello='world' 547 | ``` 548 | 549 | If "no parent" or "n", the parent function will not be shown. 550 | ``` 551 | peek.show_line_number = "n" 552 | def shout(): 553 | hello="world" 554 | peek(hello) 555 | shout() 556 | ``` 557 | prints something like 558 | ``` 559 | #5 ==> hello='world' 560 | ``` 561 | Note that if you call `peek` without any arguments, the line number is always shown, regardless of the status `show_line_number`. 562 | 563 | See below for an explanation of the information provided. 564 | 565 | ### show_time / st 566 | If True, adds the current time to `peek()`'s output. 567 | 568 | ``` 569 | peek.configure(show_time=True) 570 | hello="world" 571 | peek(hello) 572 | ``` 573 | prints something like 574 | ``` 575 | @ 13:01:47.588125 ==> hello='world' 576 | ``` 577 | 578 | ### show_delta / sd 579 | If True, adds the number of seconds since the start of the program to `peek()`'s output. 580 | ``` 581 | import time 582 | peek.show_delta = True 583 | french = "bonjour le monde" 584 | english = "hallo world" 585 | peek(english) 586 | time.sleep(1) 587 | peek(french) 588 | ``` 589 | prints something like 590 | ``` 591 | delta=0.088 ==> english='hallo world' 592 | delta=1.091 ==> french='bonjour le monde' 593 | ``` 594 | 595 | ### show_enter / se 596 | When used as a decorator or context manager, by default, peek ouputs a line when the decorated the 597 | function is called or the context manager is entered. 598 | 599 | With `show_enter=False` this line can be suppressed. 600 | 601 | ### show_exit / sx 602 | When used as a decorator or context manager, by default, peek ouputs a line when the decorated the 603 | function returned or the context manager is exited. 604 | 605 | With `show_exit=False` this line can be suppressed. 606 | 607 | 608 | ### show_traceback 609 | When show_traceback is True, the ordinary output of peek() will be followed by a printout of the 610 | traceback, similar to an error traceback. 611 | 612 | ``` 613 | def x(): 614 | peek(show_traceback=True) 615 | 616 | x() 617 | x() 618 | ``` 619 | prints something like 620 | ``` 621 | #4 in x() 622 | Traceback (most recent call last) 623 | File "c:\Users\Ruud\Dropbox (Personal)\Apps\Python Ruud\peek\x.py", line 6, in 624 | x() 625 | File "c:\Users\Ruud\Dropbox (Personal)\Apps\Python Ruud\peek\x.py", line 4, in x 626 | peek() 627 | #4 in x() 628 | Traceback (most recent call last) 629 | File "c:\Users\Ruud\Dropbox (Personal)\Apps\Python Ruud\peek\x.py", line 7, in 630 | x() 631 | File "c:\Users\Ruud\Dropbox (Personal)\Apps\Python Ruud\peek\x.py", line 4, in x 632 | peek() 633 | ``` 634 | The `show_traceback` functionality is also available when peek is used as a decorator or context manager. 635 | 636 | ### line_length / ll 637 | This attribute is used to specify the line length (for wrapping). The default is 80. 638 | Peek tries to keep all output on one line, but if it can't it will wrap: 639 | 640 | ``` 641 | d = dict(a1=1,a2=dict(a=1,b=1,c=3),a3=list(range(10))) 642 | peek(d) 643 | peek(d, line_length=160) 644 | ``` 645 | prints 646 | ``` 647 | d= 648 | {'a1': 1, 649 | 'a2': {'a': 1, 'b': 1, 'c': 3}, 650 | 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 651 | d={'a1': 1, 'a2': {'a': 1, 'b': 1, 'c': 3}, 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 652 | ``` 653 | 654 | If line_length is set to 0 or 'terminal_width', peek will use the width of the current terminal as line length. 655 | Note that not all terminals correctly return the actual width. 656 | (The terminal size is determined by calling `shutil.get_terminal_size().columns`) 657 | 658 | ### max_lines / ml 659 | 660 | This attribute is used to specify the maximum number of lines to print for one peek call. The default is 1000000, so no limitation. 661 | If there are more than max_lines to be printed, only max_lines will be printed, followed by a line `[abbreviated]`. 662 | 663 | So, 664 | ``` 665 | peek([list(range(i, i + 10)) for i in range(10, 100, 10)]) 666 | ``` 667 | prints 668 | ``` 669 | [list(range(i, i + 10)) for i in range(10, 100, 10)]= 670 | [[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 671 | [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], 672 | [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], 673 | [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], 674 | [50, 51, 52, 53, 54, 55, 56, 57, 58, 59], 675 | [60, 61, 62, 63, 64, 65, 66, 67, 68, 69], 676 | [70, 71, 72, 73, 74, 75, 76, 77, 78, 79], 677 | [80, 81, 82, 83, 84, 85, 86, 87, 88, 89], 678 | [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]] 679 | ``` 680 | But 681 | ``` 682 | peek.max_lines = 5 683 | peek([list(range(i, i + 10)) for i in range(10, 100, 10)]) 684 | ``` 685 | prints 686 | ``` 687 | list(range(i, i + 10)) for i in range(10, 100, 10)]= 688 | [[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 689 | [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], 690 | [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], 691 | [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], 692 | [abbreviated] 693 | ``` 694 | This feature can be useful on platforms, where printing many lines is time consuming, like on xlwings lite. 695 | 696 | ### color / col /c and color_value / colv / c 697 | The color attribute is used to specify the color of the output. 698 | 699 | There's a choice of `"white"`, `"black"`, `"red"`, `"green"`, `"blue"`, `"cyan"`, `"magenta"`, `"yellow"`, `" dark_white"`, `"dark_black"`, `"dark_red"`, `"dark_green"`, `"dark_blue"`, `"dark_cyan"`, `"dark_magenta"` and `"dark_yellow"`: 700 | 701 | 702 | 703 | To set the color to 'nothing'", "use "-". 704 | 705 | On top of that, color_value may be used to specify the value part of an output item. By specifying color_value as "" (the default), the value part will be displayed with the same color as the rest of the output. 706 | 707 | For instance: 708 | 709 | ``` 710 | item1 = "value1" 711 | item2 = "value2" 712 | peek.color="yellow" 713 | peek(item1, item2) 714 | peek(item1, item2, color_value="green") 715 | peek(item1, item2, color="red") 716 | peek(item1, item2, color="red", color_value="green") 717 | ``` 718 | 719 | will result in: 720 | 721 | 722 | 723 | Alternatively, the color/Color_value attribute can be specified as an integer, where 724 | - 0 - (reset) 725 | - 1 white 726 | - 2 black 727 | - 3 red 728 | - 4 blue 729 | - 5 green 730 | - 6 yellow 731 | - 7 magenta 732 | - 8 cyan (tealblue) 733 | 734 | Note that the color number corresponds to the number of letters in the name (apart from white and black). 735 | A negative color/color_value number represents the dark version, e.g. `peek((a:=123), c=3, cv=-5)` will print `(a:=3)=` in red and `123` in dark_green. 736 | 737 | Of course, color and color_value may be specified in a peek.toml file, to make all peek output in a specified color. 738 | 739 | 740 | ------ 741 | 742 | Bonus feature 743 | 744 | peek offers direct access to ANSI color escape sequences with `peek.ANSI.black`, `peek.ANSI.white`, `peek.ANSI.red`, `peek.ANSI.green`, `peek.ANSI.blue`, `peek.ANSI.cyan`, `peek.ANSI.magenta`, `peek.ANSI.yellow`, `peek.ANSI.light_black`, `peek.ANSI.light_white`, `peek.ANSI.light_red`, `peek.ANSI.light_green`, `peek.ANSI.light_blue`, `peek.ANSI.light_cyan`, `peek.ANSI.light_magenta`, `peek.ANSI.light_yellow` and `peek.ANSI.reset`. 745 | 746 | E.g. 747 | 748 | ``` 749 | peek(repr(peek.ANSI.red)) 750 | ``` 751 | 752 | will show 753 | 754 | ``` 755 | repr(peek.ANSI.red)='\x1b[1;31m' 756 | ``` 757 | 758 | ------ 759 | 760 | ### use_color 761 | 762 | Colors can be ignored completely by using `peek.use_color = False`. 763 | 764 | So, 765 | 766 | ``` 767 | peek(hello, color="red") 768 | peek.use_color = False 769 | peek(hello, color="red") 770 | ``` 771 | 772 | will print `hello=world` once in red and once without color. 773 | 774 | Of course, `use_color` can be specified in a peek.toml file. 775 | 776 | ### compact 777 | 778 | This attribute is used to specify the compact parameter for `pformat` (see the pprint documentation 779 | for details). `compact` is False by default. 780 | 781 | ``` 782 | a = 9 * ["0123456789"] 783 | peek.line_length = 80 784 | peek(a) 785 | peek(a, compact=True) 786 | ``` 787 | prints 788 | ``` 789 | a= 790 | ['0123456789', 791 | '0123456789', 792 | '0123456789', 793 | '0123456789', 794 | '0123456789', 795 | '0123456789', 796 | '0123456789', 797 | '0123456789', 798 | '0123456789'] 799 | a= 800 | ['0123456789', '0123456789', '0123456789', '0123456789', '0123456789', 801 | '0123456789', '0123456789', '0123456789', '0123456789'] 802 | ``` 803 | 804 | ### indent 805 | This attribute is used to specify the indent parameter for `pformat` (see the pprint documentation 806 | for details). `indent` is 1 by default. 807 | 808 | ``` 809 | s = "01234567890012345678900123456789001234567890" 810 | peek.line_length = 80 811 | peek( [s, [s]]) 812 | peek( [s, [s]], indent=4) 813 | ``` 814 | prints 815 | ``` 816 | [s, [s]]= 817 | ['01234567890012345678900123456789001234567890', 818 | ['01234567890012345678900123456789001234567890']] 819 | [s, [s]]= 820 | [ '01234567890012345678900123456789001234567890', 821 | ['01234567890012345678900123456789001234567890']] 822 | ``` 823 | 824 | ### depth 825 | This attribute is used to specify the depth parameter for `pformat` (see the pprint documentation 826 | for details). `depth` is `1000000` by default. 827 | 828 | ``` 829 | s = "01234567890012345678900123456789001234567890" 830 | peek([s,[s,[s,[s,s]]]]) 831 | peek([s,[s,[s,[s,s]]]], depth=3) 832 | ``` 833 | prints 834 | ``` 835 | [s,[s,[s,[s,s]]]]= 836 | ['01234567890012345678900123456789001234567890', 837 | ['01234567890012345678900123456789001234567890', 838 | ['01234567890012345678900123456789001234567890', 839 | ['01234567890012345678900123456789001234567890', 840 | '01234567890012345678900123456789001234567890']]]] 841 | [s,[s,[s,[s,s]]]]= 842 | ['01234567890012345678900123456789001234567890', 843 | ['01234567890012345678900123456789001234567890', 844 | ['01234567890012345678900123456789001234567890', [...]]]] 845 | ``` 846 | 847 | ### wrap_indent 848 | This specifies the indent string if the output does not fit in the line_length (has to be wrapped). 849 | Rather than a string, wrap_indent can be also be an integer, in which case the wrap_indent will be that amount of blanks. 850 | The default is 4 blanks. 851 | 852 | E.g. 853 | ``` 854 | d = dict(a1=1,a2=dict(a=1,b=1,c=3),a3=list(range(10))) 855 | peek.line_length = 80 856 | peek(d, wrap_indent=" ") 857 | peek(d, wrap_indent="....") 858 | peek(d, wrap_indent=2) 859 | ``` 860 | prints 861 | ``` 862 | d= 863 | {'a1': 1, 864 | 'a2': {'a': 1, 'b': 1, 'c': 3}, 865 | 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 866 | d= 867 | ....{'a1': 1, 868 | .... 'a2': {'a': 1, 'b': 1, 'c': 3}, 869 | .... 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 870 | d= 871 | {'a1': 1, 872 | 'a2': {'a': 1, 'b': 1, 'c': 3}, 873 | 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 874 | ``` 875 | 876 | ### enabled 877 | Can be used to disable the output: 878 | 879 | ``` 880 | peek.enabled = False 881 | s = 'the world is ' 882 | peek(s + 'perfect.') 883 | peek.enabled = True 884 | peek(s + 'on fire.') 885 | ``` 886 | prints 887 | ``` 888 | s + 'on fire.'='the world is on fire.' 889 | ``` 890 | and nothing about a perfect world. 891 | 892 | ### sort_dicts 893 | By default, peek does not sort dicts (printed by pprint). However, it is possible to get the 894 | default pprint behaviour (i.e. sorting dicts) with the sorted_dicts attribute: 895 | 896 | ``` 897 | world = {"EN": "world", "NL": "wereld", "FR": "monde", "DE": "Welt"} 898 | peek(world)) 899 | peek(world, sort_dicts=False) 900 | peek(world, sort_dicts=True) 901 | ``` 902 | prints 903 | ``` 904 | world={'EN': 'world', 'NL': 'wereld', 'FR': 'monde', 'DE': 'Welt'} 905 | world={'EN': 'world', 'NL': 'wereld', 'FR': 'monde', 'DE': 'Welt'} 906 | world={'DE': 'Welt', 'EN': 'world', 'FR': 'monde', 'NL': 'wereld'} 907 | ``` 908 | 909 | ### underscore_numbers / un 910 | 911 | By default, peek does not add underscores in big numbers (printed by pprint). However, it is possible to get the 912 | default pprint behaviour with the underscore_numbers attribute: 913 | 914 | ``` 915 | numbers = dict(one= 1, thousand= 1000, million=1000000, x1234567890= 1234567890) 916 | peek(numbers) 917 | peek(numbers, underscore_numbers=True) 918 | peek(numbers, un=False) 919 | ``` 920 | prints 921 | ``` 922 | numbers={'one': 1, 'thousand': 1000, 'million': 1000000, 'x1234567890': 1234567890} 923 | numbers={'one': 1, 'thousand': 1_000, 'million': 1_000_000, 'x1234567890': 1_234_567_890} 924 | numbers={'one': 1, 'thousand': 1000, 'million': 1000000, 'x1234567890': 1234567890} 925 | ``` 926 | 927 | > [!NOTE] 928 | > 929 | > underscore_numbers is ignored under Python 3.9 930 | 931 | ### seperator / sep 932 | 933 | By default, pairs (on one line) are separated by `", "`. 934 | It is possible to change this with the attribute ` separator`: 935 | 936 | ``` 937 | a = "abcd" 938 | b = 1 939 | c = 1000 940 | d = list("peek") 941 | peek(a, (b, c), d) 942 | peek(a, (b, c), d, separator=" | ") 943 | ``` 944 | prints 945 | ``` 946 | a='abcd', (b,c)=(1, 1000), d=['p', 'e', 'e', 'k'] 947 | a='abcd' | (b,c)=(1, 1000) | d=['p', 'e', 'e', 'k'] 948 | ``` 949 | 950 | ### context_separator 951 | 952 | By default the line_number, time and/or delta are followed by ` ==> `. 953 | It is possible to change this with the attribute `context_separator`: 954 | 955 | ``` 956 | a = "abcd" 957 | peek(a,show_time=True) 958 | peek(a, show_time=True, context_separator = ' \u279c ') 959 | ``` 960 | prints: 961 | ``` 962 | @ 09:05:38.693840 ==> a='abcd' 963 | @ 09:05:38.707914 ➜ a='abcd' 964 | ``` 965 | ### equals_separator 966 | By default name of a variable and its value are separated by `= `. 967 | It is possible to change this with the attribute `equals_separator`: 968 | 969 | ``` 970 | a = "abcd" 971 | peek(a) 972 | peek(a, equals_separator = ' == ") 973 | ``` 974 | prints: 975 | ``` 976 | a='abcd' 977 | a == 'abcd' 978 | ``` 979 | ### quote_string / qs 980 | If True (the default setting) strings will be displayed with surrounding quotes (like repr). 981 | If False, string will be displayed without surrounding quotes (like str). 982 | E.g. 983 | 984 | ``` 985 | test='test' 986 | peek('==>', test) 987 | peek('==>', test, quote_string=False) 988 | ``` 989 | This will print: 990 | ``` 991 | '==>', test='test' 992 | ==>, test=test 993 | ``` 994 | > [!NOTE] 995 | > 996 | > This setting does not influence how strings are displayed within other data structures, like dicts and lists. 997 | 998 | ### format / fmt 999 | With the format attribute, it is possible to apply a format specifier to each of the values to be printed, like 1000 | ``` 1001 | test_float = 1.3 1002 | peek(test_float, format="06.3f") 1003 | ``` 1004 | This will print 1005 | ``` 1006 | test_float=01.300 1007 | ``` 1008 | 1009 | The format should be like the Python format specifiers, with or without the `:` prefix, like `"6.3f"`, `">10"`, `"06d"`, `:6.3d`. 1010 | It is also possible to use the `!` format specifier: `"!r"`, `"!r:>10"`. 1011 | 1012 | If format is the null string (`""`) (the default), this functionality is skipped completely. 1013 | 1014 | It is also possible to use a list (or tuple) of format specifiers, which are tried in succession. If they all fail, the 'normal' serializer will be used. 1015 | 1016 | ``` 1017 | test_float = 1.3 1018 | test_integer=10 1019 | test_string = "test" 1020 | test_dict=dict(one=1, two=2) 1021 | peek(test_float, test_integer, test_string, test_dict, format=["04d", "06.3f", ">10"]) 1022 | ``` 1023 | 1024 | will result in 1025 | 1026 | ``` 1027 | test_float=01.300, test_integer=0010, test_string= test, test_dict={'one': 1, 'two': 2} 1028 | ``` 1029 | 1030 | Of course, format may be put in a peek.toml file. 1031 | 1032 | ### values_only / vo 1033 | 1034 | If False (the default), both the left-hand side (if possible) and the 1035 | value will be printed. If True, the left hand side will be suppressed: 1036 | 1037 | ``` 1038 | hello = "world" 1039 | peek(hello, 2 * hello) 1040 | peek(hello, 2 * hello, values_only=True) 1041 | ``` 1042 | prints 1043 | ``` 1044 | hello='world', 2 * hello='worldworld' 1045 | 'world', 'worldworld' 1046 | ``` 1047 | 1048 | ### values_only_for_fstrings / voff 1049 | If False (the default), both the original f-string and the 1050 | value will be printed for f-strings. 1051 | If True, the left_hand side will be suppressed in case of an f-string: 1052 | 1053 | ``` 1054 | x = 12.3 1055 | peek.quote_string = False 1056 | peek(f"{x=:0.3e}") 1057 | peek.values_only_for_fstrings = True 1058 | peek(f"{x=:0.3e}") 1059 | ``` 1060 | prints 1061 | ``` 1062 | f"{x=:0.3e}"=x=1.230e+01 1063 | x=1.230e+01 1064 | ``` 1065 | Note that if `values_only` is True, f-string will be suppressed, regardless of `values_only_for_fstrings`. 1066 | 1067 | 1068 | ### end 1069 | 1070 | The `end` attribute works like the end parameter of print. By default, `end` is "\n". 1071 | This can be useful to have several peek outputs on one line, like: 1072 | 1073 | ``` 1074 | for i in range(5): 1075 | peek(i*i, end=' ') 1076 | peek('') 1077 | ``` 1078 | Maybe more useful is to show the output change on the same line, e.g. a status. 1079 | ``` 1080 | import time 1081 | for i in range(50): 1082 | peek.print(f"time {time.time():10.2f}",end="", prefix="\r") 1083 | time.sleep(0.1) 1084 | peek.print() 1085 | ``` 1086 | > [!NOTE] 1087 | > 1088 | > The `end` parameter will not be only applied when output is "logging.debug", "logging.info", "logging.warning", "logging.error" or "logging.critical". 1089 | 1090 | > [!NOTE] 1091 | > 1092 | > `Under Pythonista \r` has to be at the start of a peek.print output. Otherwise, it does not work. 1093 | 1094 | ### return_none 1095 | Normally, `peek()`returns the values passed directly, which is usually fine. However, when used in a notebook 1096 | or REPL, that value will be shown, and that can be annoying. Therefore, if `return_none`is True, `peek()`will 1097 | return None and thus not show anything. 1098 | 1099 | ``` 1100 | a = 3 1101 | b = 4 1102 | print(peek(a, b)) 1103 | peek.return_none = True 1104 | print(peek(a, b)) 1105 | ``` 1106 | prints 1107 | ``` 1108 | a=3, b=4 1109 | (3, 4) 1110 | a=3, b=4 1111 | None 1112 | ``` 1113 | 1114 | ### delta 1115 | The delta attribute can be used to (re)set the current delta, e.g. 1116 | ``` 1117 | peek.delta = 0 1118 | print(peek.delta) 1119 | ``` 1120 | prints a value that id slightly more than 0. 1121 | 1122 | ### print_like / print 1123 | When print_like (or print) is False, peek will work by expanding the arguments to description/serialized value pairs. 1124 | But, when print_like is True, peek becomes a kind of supercharged print: 1125 | 1126 | ``` 1127 | peek.print_like = True 1128 | peek(12, f"{min(1, 2)=}", list(range(4), color="yellow") 1129 | ``` 1130 | will print 1131 | ``` 1132 | 12 min(1, 2)=1 [0, 1, 2, 3] 1133 | ``` 1134 | in yellow, but if peek.enabled is False, nothing will be printed. 1135 | 1136 | You can also use peek.print (see below). 1137 | 1138 | > [!TIP] 1139 | > 1140 | > Of course, `print_like` can be put in a **peek.toml** file. 1141 | 1142 | ## Use peek.print to use peek like print with extras 1143 | The method `peek.print` allows peek to be used as alternative to print. Note that `peek.print` applies the `color`, `context_separator`, `enabled`, `end`, `filter` and `output`, `separator_print`, `show_delta` and `show_time`. It is also possible to redirect the output to a string with `as_str`. 1144 | 1145 | So, 1146 | 1147 | ``` 1148 | peek.filter = "level==1" 1149 | peek.print(f"{max(1, 2)=}", color="blue") # default level is 0, so this will be suppressed 1150 | peek.print(f"{min(1, 2)=}", color="red", level=1) 1151 | ``` 1152 | 1153 | will print 1154 | 1155 | ``` 1156 | min(1, 2)=1 1157 | ``` 1158 | 1159 | in red, but only if peek.enabled is True (which is the default). 1160 | 1161 | In order to behave similar to print, `peek` has an extra attribute, `separator_print` (alias: `sepp`). This attribute (default " ") will be used when `peek.printing`. 1162 | When calling `peek.print`, `sep` may be used instead. So 1163 | 1164 | ``` 1165 | peek.sepp = "|" 1166 | peek.print("test") 1167 | ``` 1168 | 1169 | Has the same effect as 1170 | 1171 | ``` 1172 | peek.print("test", sep="|") 1173 | ``` 1174 | 1175 | and 1176 | 1177 | ``` 1178 | peek.print("test", sepp="|") 1179 | ``` 1180 | 1181 | but not the same as 1182 | 1183 | ``` 1184 | peek.sep = "|" # sets the 'normal' peek separator 1185 | ``` 1186 | > [!NOTE] 1187 | > 1188 | > `peek.print` does not obey the line length and will always return None (unless as_str is True). 1189 | 1190 | 1191 | ## Peeking locals and globals 1192 | It is possible to get the names and values of all local or global variables. 1193 | 1194 | To do that, just put `locals` or `globals` in the call to peek, e.g.: 1195 | 1196 | ``` 1197 | def my_func(): 1198 | a = 10 1199 | b = a * a 1200 | peek(locals) 1201 | my_func() 1202 | ``` 1203 | 1204 | will print all local variables, apart from those starting with `__`, so: 1205 | ``` 1206 | a=10, b=100 1207 | ``` 1208 | 1209 | Likewise, 1210 | ``` 1211 | peek(globals) 1212 | ``` 1213 | will print all global variables, apart from those starting with `__` 1214 | 1215 | > [!IMPORTANT] 1216 | > 1217 | > You should not add parentheses after `locals` or `globals` for peek to work properly! 1218 | 1219 | ## Return a string instead of sending to output 1220 | 1221 | `peek(*args, as_str=True)` is like `peek(*args)` but the output is returned as a string instead 1222 | of written to output. 1223 | 1224 | ``` 1225 | hello = "world" 1226 | s = peek(hello, as_str=True) 1227 | print(s, end="") 1228 | ``` 1229 | prints 1230 | ``` 1231 | hello='world' 1232 | ``` 1233 | 1234 | Note that if enabled=False, the call will return the null string (`""`). 1235 | 1236 | By default, a string will contain embedded ANSI color escape strings if either `color` or `color_value` specifies a color. By setting `use_color` to False, these escape sequences will be suppressed. 1237 | 1238 | ``` 1239 | hello = "world" 1240 | s = peek(hello, color="red", color_value="green", as_str=True) 1241 | print(repr(s)) 1242 | peek.use_color = False 1243 | s = peek(hello, color="red", color_value="green", as_str=True) 1244 | print(repr(s)) 1245 | ``` 1246 | prints 1247 | ``` 1248 | '\x1b[1;31mhello=\x1b[1;32mworld\x1b[1;31m\x1b[0m\n' 1249 | 'hello=world\n' 1250 | ``` 1251 | 1252 | ## Disabling peek's output 1253 | 1254 | ``` 1255 | peek1 = peek.fork(show_delta=True) 1256 | peek(1) 1257 | peek1(2) 1258 | peek.enabled = False 1259 | peek(3) 1260 | peek1(4) 1261 | peek1.enabled = True 1262 | peek(5) 1263 | peek1(6) 1264 | print(peek1.enabled) 1265 | ``` 1266 | prints 1267 | ``` 1268 | 1 1269 | delta=0.011826 ==> 2 1270 | 5 1271 | delta=0.044893 ==> 6 1272 | True 1273 | ``` 1274 | Of course `peek()` continues to return its arguments when disabled. 1275 | 1276 | It is also possible to suppress output with the provided attribute (see above). 1277 | 1278 | ## Using filter to control peek output 1279 | 1280 | It is possible to define a filter function that determines whether peek output should be suppressed. 1281 | By default, the filter is defined as "" denoting no filter. 1282 | 1283 | Suppose we add a (float) level to a peek statement. By default, this level is 0. E.g.: 1284 | 1285 | ``` 1286 | peek("critical", level=0) 1287 | peek("important", level=1) 1288 | peek("warning", level=2) 1289 | ``` 1290 | 1291 | With `peek.filter = "level <= 1"` the program makes sure that level=2 is not displayed at all. 1292 | 1293 | It is possible to use more than one attribute, like 1294 | 1295 | ``` 1296 | peek.filter = "color == 'blue' and delta > 5" 1297 | ``` 1298 | As an alternative to `enabled` we can also say 1299 | ``` 1300 | peek.filter = "False" 1301 | ``` 1302 | 1303 | ## Copying to the clipboard 1304 | 1305 | It is possible to copy a value to the clipboard. There are two ways: 1306 | 1307 | #### With peek(to_clipboard=True) 1308 | 1309 | With the optional keyword argument, *to_clipboard*: 1310 | 1311 | - If to_clipboard==False (the default), nothing is copied to the clipboard. 1312 | - If to_clipboard==True, the *value* of the the *last* parameter will be copied to the clipboard. The output itself is as usual. 1313 | 1314 | Examples: 1315 | 1316 | ``` 1317 | part1 = 200 1318 | extra = "extra" 1319 | peek(part1, extra, to_clipboard=True) 1320 | # will print part1=200, extra='extra' and copy `extra` to the clipboard 1321 | peek(200, to_clipboard=True)\ 1322 | # will print 200 and copy 200 to the clipboard 1323 | peek(to_clipboard=True) 1324 | # will print #5 (or similar) and empty the clipboard 1325 | ``` 1326 | 1327 | Note that *to_clipboard* is a peek attribute. 1328 | 1329 | If as_str==True, to_clipboard is ignored. 1330 | 1331 | #### With peek.to_clipboard 1332 | 1333 | Just use peek.to_clipboard to copy any value to the clipboard. So, 1334 | ``` 1335 | part1 = 1234 1336 | peek.to_clipboard(part1) 1337 | ``` 1338 | 1339 | will copy `1234` to the clipboard and write `copied to clipboard: 1234` to the console. 1340 | If the confirmation message is not wanted, just add confirm=False, like 1341 | 1342 | ``` 1343 | peek.to_clipboard(part1, confirm=False) 1344 | ``` 1345 | 1346 | #### General 1347 | 1348 | Implementation detail: the clipboard functionality uses pyperclip, apart from under Pythonista, where the builtin clipboard module is used. 1349 | The pyperclip module is not installed automatically when peek-python is installed. So, it might be necessary to do 1350 | 1351 | ``` 1352 | pip install pyperclip 1353 | ``` 1354 | 1355 | This functionality is particularly useful for entering an answer of an *Advent of Code* solution to the site. 1356 | 1357 | ## Conditional stop of program 1358 | 1359 | With `peek.stop` or `peek.stop()` a program will be stopped (by raising a `SystemExit` exception), provided `peek.enabled` is True. 1360 | If `peek.enabled` is False, the program will just continue. 1361 | 1362 | For example: 1363 | ``` 1364 | peek.enabled = False 1365 | peek(12) 1366 | peek.stop 1367 | peek.enabled = True 1368 | peek(13) 1369 | peek.stop 1370 | peek(14) 1371 | ``` 1372 | will print: 1373 | ``` 1374 | 13 1375 | stopped by peek.stop 1376 | ``` 1377 | 1378 | and then stop execution. 1379 | 1380 | > [!NOTE] 1381 | > 1382 | > Under Pythonista, `sys.exit()` will be called. 1383 | > Under pyodide/xlwings lite, an `Exception` exception will be raised. 1384 | 1385 | ## Interpreting the line number information 1386 | 1387 | When `show_line_number` is True or peek() is used without any parameters, the output will contain the line number like: 1388 | ``` 1389 | #3 ==> a='abcd' 1390 | ``` 1391 | If the line resides in another file than the main file, the filename (without the path) will be shown as well: 1392 | ``` 1393 | #30[foo.py] ==> foo='Foo' 1394 | ``` 1395 | And finally when used in a function or method, that function/method will be shown as well: 1396 | ``` 1397 | #456[foo.py] in square_root ==> x=123 1398 | ``` 1399 | The parent function can be suppressed by setting `show_line_number` or `sln` to `"n"` or `"no parent"`. 1400 | 1401 | ## Configuring at import time 1402 | 1403 | It can be useful to configure peek at import time. This can be done by providing a `peek.toml` file which 1404 | can contain any attribute configuration overriding the standard settings. 1405 | E.g. if there is a `peek.toml` file with the following contents 1406 | 1407 | ``` 1408 | outpout = "stderr" 1409 | show_time = true 1410 | ll = 160 1411 | quote_string = false 1412 | ``` 1413 | in the same folder as the application, this program: 1414 | ``` 1415 | hello = "world" 1416 | peek(hello) 1417 | ``` 1418 | will print something like this to stderr (rather than stdout): 1419 | ``` 1420 | @ 14:53:41.392190 ==> hello=world 1421 | ``` 1422 | At import time current directory will be searched for `peek.toml` and if not found, one level up, etc. until the root directory is reached. 1423 | 1424 | Please observe that toml values are slightly different from their Python equivalents: 1425 | ``` 1426 | ----------------------------------- 1427 | Python toml 1428 | ----------------------------------- 1429 | True true 1430 | False false 1431 | strings preferably double quoted 1432 | ----------------------------------- 1433 | ``` 1434 | Note that not-specified attributes will remain the default settings. 1435 | 1436 | Just for your information, the core developer of peek uses a peek.toml file with the contents: 1437 | ``` 1438 | line_length = 160 1439 | color = "yellow" 1440 | quote_string = false 1441 | ``` 1442 | 1443 | On top of this toml functionality, default values may be also overridden by environment variables. 1444 | They should be specified as `peek.`, like 1445 | 1446 | ``` 1447 | ------------------------------ 1448 | environment variable value 1449 | ------------------------------ 1450 | peek.line_length 160 1451 | peek.color "yellow" 1452 | peek.show_time true 1453 | ------------------------------ 1454 | ``` 1455 | The value should follow the same rules as in a toml-file. 1456 | 1457 | Note that the environment variables are read *before* reading a *peek.toml* file. 1458 | 1459 | This functionality is particularly useful for using peek in xlwings lite, as there's no local file system to store a toml file, there. 1460 | 1461 | ## Working with multiple instances of peek 1462 | 1463 | Normally, only the `peek` object is used. 1464 | 1465 | It can be useful to have multiple instances, e.g. when some of the debugging has to be done with context information 1466 | and others requires an alternative prefix. 1467 | 1468 | There are several ways to obtain a new instance of peek: 1469 | 1470 | * by using `peek.new()` 1471 | 1472 | With this a new peek object is created with the default attributes 1473 | * by using `peek.new(ignore_toml=True)` 1474 | 1475 | With this a new peekobject is created with the default attributes. Any peek.toml files are ignored. 1476 | * by using `peek.fork()` 1477 | 1478 | With this a new peek object is created with the same attributes as the object it is created ('the parent') from. Note that any non set attributes are copied (propagated) from the parent. 1479 | * by using `peek.clone()`, which copies all attributes from peek() 1480 | 1481 | With this a new peek object is created with the same attributes as the object it is created ('the parent') from. Note that the attributes are not propagated from the parent, in this case. 1482 | 1483 | * with `peek()` used as a context manager 1484 | 1485 | In either case, attributes can be added to override the default ones. 1486 | 1487 | #### Example 1488 | ``` 1489 | peek_with_line_number = peek.fork(show_line_number=True) 1490 | peek_with_new_prefix = peek.new(prefix="==> ") 1491 | peek_with_new_prefix_and_time = peek_with_new_prefix.clone(show_time=True) 1492 | hello="world" 1493 | peek_with_line_number(hello) 1494 | peek_with_new_prefix(hello) 1495 | peek_with_new_prefix_and_time(hello) 1496 | peek.equals_separator = " == " # this affects only the forked objects 1497 | peek_with_line_number(hello) 1498 | peek_with_new_prefix(hello) 1499 | peek_with_new_prefix_and_time(hello) 1500 | with peek(prefix="peek_cm ") as peek_cm: 1501 | peek_cm(hello) 1502 | peek(hello) 1503 | ``` 1504 | prints something like 1505 | ``` 1506 | #28 ==> hello='world' 1507 | ==> hello='world' 1508 | ==> @ 09:55:52.122818 ==> hello='world' 1509 | #32 ==> hello == 'world' 1510 | ==> hello='world' 1511 | ==> @ 09:55:52.125928 ==> hello='world' 1512 | peek_cm enter 1513 | peek_cm hello == 'world' 1514 | hello == 'world' 1515 | peek_cm exit in 0.001843 seconds 1516 | ``` 1517 | 1518 | ### ignore_toml 1519 | With `peek.new(ignore_toml=True)` an instance of peek without having applied any toml configuration file will be returned. That can be useful when guaranteeing the same output in several setups. 1520 | 1521 | #### Example 1522 | Suppose we have a `peek.toml` file in the current directory with the contents 1523 | ``` 1524 | {prefix="==>"} 1525 | ``` 1526 | Then 1527 | ``` 1528 | peek_post_toml = peek.new() 1529 | peek_ignore_toml = peek.new(ignore_toml=True) 1530 | hello = "world" 1531 | peek(hello) 1532 | peek_post_toml(hello) 1533 | peek_ignore_toml(hello) 1534 | ``` 1535 | prints 1536 | ``` 1537 | ==>hello='world' 1538 | ==>hello='world' 1539 | hello='world' 1540 | ``` 1541 | 1542 | ## Test script 1543 | 1544 | On GitHub is a file `test_peek.py` that tests (and thus also demonstrates) most of the functionality 1545 | of peek. 1546 | 1547 | It is very useful to have a look at the tests to see the features (some may be not covered (yet) in this readme). 1548 | 1549 | ## Using peek in a REPL 1550 | 1551 | Peek may be used in a REPL, but with limited functionality: 1552 | * all arguments are just presented as such, i.e. no left-hand side, e.g. 1553 | ``` 1554 | >> hello = "world" 1555 | >>> peek(hello, hello * 2) 1556 | 'hello', 'hellohello' 1557 | ('hello', 'hellohello') 1558 | ``` 1559 | * line numbers are never shown 1560 | * use as a decorator is not supported 1561 | * use as a context manager is not supported 1562 | 1563 | > [!NOTE] 1564 | > 1565 | > Under Python >=3.13 most of the normal peek functionality is available in the REPL. A reason to upgrade! 1566 | 1567 | 1568 | ## Limitations 1569 | 1570 | It is not possible to use peek: 1571 | * from a frozen application (e.g. packaged with PyInstaller) 1572 | * when the underlying source code has changed during execution 1573 | 1574 | ## Changelog 1575 | 1576 | The changelog can be found here: 1577 | 1578 | * https://github.com/salabim/peek/blob/main/changelog.md or 1579 | * https://salabim.org/peek/changelog 1580 | 1581 | 1582 | ## Acknowledgement 1583 | 1584 | The **peek** package is inspired by the **IceCream** package, but is a nearly complete rewrite. See https://github.com/gruns/icecream 1585 | 1586 | Many thanks to the author Ansgar Grunseid / grunseid.com / grunseid@gmail.com . 1587 | 1588 | The peek package is a rebrand of the **ycecream** package, with many enhancements. 1589 | 1590 | ## Differences with IceCream 1591 | 1592 | The peek module was originally a fork of **IceCream**, but has many differences: 1593 | 1594 | ``` 1595 | ----------------------------------------------------------------------------------------- 1596 | characteristic peek IceCream 1597 | ----------------------------------------------------------------------------------------- 1598 | default name peek ic 1599 | import method import peek from icecream import ic 1600 | number of files 1 several 1601 | usable without installation yes no 1602 | can be used as a decorator yes no 1603 | can be used as a context manager yes no 1604 | can show traceback yes no 1605 | can be used like print w/extras yes (with peek.print) no 1606 | allows non linefeed printing yes (via end parameter) requires patching 1607 | PEP8 (Pythonic) API yes no 1608 | format specification optional no 1609 | sorts dicts no by default, optional yes 1610 | supports compact, indent, 1611 | and underscore_numbers 1612 | parameters of pprint yes no 1613 | use from a REPL limited functionality no 1614 | external configuration via toml file no 1615 | level control yes no 1616 | observes line_length correctly yes no 1617 | benchmarking functionality yes no 1618 | can peek locals or globals yes no 1619 | suppress f-strings at left hand optional no 1620 | indentation 4 blanks (overridable) length of prefix 1621 | forking and cloning yes no 1622 | handling of source problems peeks only the value warning issued 1623 | test script pytest unittest 1624 | colorize *) yes, off by default yes, on by default 1625 | ----------------------------------------------------------------------------------------- 1626 | *) peek allows selection of colors, whereas IceCream does coloring based on contents. 1627 | ``` 1628 | ## Contact info 1629 | You can contact Ruud van der Ham, the core developer, via ruud@salabim.org . 1630 | 1631 | ## Badges 1632 | 1633 | ![PyPI](https://img.shields.io/pypi/v/peek-python) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/peek-python) ![PyPI - Implementation](https://img.shields.io/pypi/implementation/peek-python) 1634 | ![PyPI - License](https://img.shields.io/pypi/l/peek-python) ![ruff](https://img.shields.io/badge/style-ruff-41B5BE?style=flat) 1635 | ![GitHub last commit](https://img.shields.io/github/last-commit/salabim/peek) 1636 | 1637 | --------------------------------------------------------------------------------