├── .gitignore ├── tests ├── __init__.py ├── utils_test.py ├── reflect_test.py ├── value_test.py └── interface_test.py ├── LICENSE.txt ├── pout ├── compat.py ├── __init__.py ├── path.py ├── environ.py ├── __main__.py ├── utils.py ├── reflect.py └── interface.py ├── pyproject.toml ├── docs └── CUSTOMIZE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tmproj 3 | *.pyc -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | #import imp 4 | import importlib 5 | import logging 6 | 7 | from testdata import TestCase, SkipTest 8 | import testdata 9 | 10 | from pout.path import SitePackagesDir 11 | from pout.compat import builtins 12 | from pout import environ 13 | 14 | 15 | try: 16 | # https://stackoverflow.com/a/50028745/5006 17 | pout2 = importlib.machinery.PathFinder().find_spec( 18 | "pout", 19 | [SitePackagesDir()] 20 | ) 21 | 22 | except ImportError: 23 | pout2 = None 24 | # for k in sys.modules.keys(): 25 | # if "pout" in k: 26 | # print(k) 27 | 28 | if hasattr(builtins, "pout"): 29 | del builtins.pout 30 | 31 | if pout2: 32 | builtins.pout2 = pout2 33 | 34 | 35 | class TestCase(TestCase): 36 | @classmethod 37 | def setUpClass(cls): 38 | environ.SHOW_COLOR = False 39 | 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012+ Jay Marcyes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /pout/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | 5 | # ripped from https://github.com/kennethreitz/requests/blob/master/requests/compat.py 6 | _ver = sys.version_info 7 | is_py2 = (_ver[0] == 2) 8 | is_py3 = (_ver[0] == 3) 9 | 10 | if is_py2: 11 | basestring = basestring 12 | unicode = unicode 13 | range = xrange # range is now always an iterator 14 | 15 | from collections import Callable, Iterable, Set, KeysView 16 | import Queue as queue 17 | import thread as _thread 18 | import __builtin__ as builtins 19 | try: 20 | from cStringIO import StringIO 21 | except ImportError: 22 | from StringIO import StringIO 23 | 24 | import inspect 25 | inspect.getfullargspec = inspect.getargspec 26 | 27 | 28 | elif is_py3: 29 | basestring = (str, bytes) 30 | unicode = str 31 | 32 | range = range 33 | 34 | from collections.abc import Callable, Iterable, Set, KeysView 35 | import queue 36 | import _thread 37 | from io import StringIO 38 | import inspect 39 | import builtins 40 | 41 | 42 | String = unicode if is_py2 else str 43 | Bytes = str if is_py2 else bytes 44 | 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | requires-python = ">=3.10" 3 | description = "Prints out python variables in an easy to read way, handy for debugging" 4 | authors = [ 5 | { name = "Jay Marcyes", email = "jay@marcyes.com" } 6 | ] 7 | classifiers = [ 8 | "Development Status :: 5 - Production/Stable", 9 | "Environment :: Plugins", 10 | "Intended Audience :: Developers", 11 | "Operating System :: OS Independent", 12 | "Topic :: Software Development :: Testing", 13 | "Programming Language :: Python :: 3" 14 | ] 15 | name = "pout" 16 | dynamic = [ 17 | "version" 18 | ] 19 | readme = "README.md" 20 | license = "MIT" 21 | license-files = [ 22 | "LICENSE.txt" 23 | ] 24 | 25 | [project.urls] 26 | Homepage = "https://github.com/Jaymon/pout" 27 | Repository = "https://github.com/Jaymon/pout" 28 | 29 | [project.optional-dependencies] 30 | tests = [ 31 | "testdata" 32 | ] 33 | 34 | [project.scripts] 35 | pout = "pout.__main__:main" 36 | 37 | [build-system] 38 | requires = [ 39 | "setuptools>=62.3.0" 40 | ] 41 | build-backend = "setuptools.build_meta" 42 | 43 | [tool.setuptools.packages.find] 44 | exclude = [ 45 | "tests*", 46 | "example*", 47 | "*_test*", 48 | "docs*" 49 | ] 50 | include = [ 51 | "pout*" 52 | ] 53 | 54 | [tool.setuptools.dynamic] 55 | version = { attr = "pout.__version__" } 56 | 57 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pout 4 | from pout.compat import * 5 | from pout.utils import String, Color 6 | from pout import environ 7 | 8 | from . import TestCase, SkipTest 9 | 10 | 11 | class StringTest(TestCase): 12 | def test___format__(self): 13 | """Make sure the String class can be formatted back and forth""" 14 | # if no exceptions are raised then the test passes 15 | s = String("poche, \u00E7a !") 16 | "{}".format(s) 17 | 18 | def test_indent(self): 19 | s = String("foo") 20 | self.assertTrue(s.indent(".", 3).startswith("...")) 21 | self.assertFalse(s.indent(".", 2).startswith("...")) 22 | 23 | def test_camelcase(self): 24 | s = String("foo_bar").camelcase() 25 | self.assertEqual("FooBar", s) 26 | 27 | def test_snakecase(self): 28 | s = String("FooBar").snakecase() 29 | self.assertEqual("foo_bar", s) 30 | 31 | 32 | class ColorTest(TestCase): 33 | @classmethod 34 | def setUpClass(cls): 35 | environ.SHOW_COLOR = True 36 | 37 | def test_has_color_support(self): 38 | self.assertTrue(environ.has_color_support()) 39 | environ.SHOW_COLOR = False 40 | self.assertFalse(environ.has_color_support()) 41 | environ.SHOW_COLOR = True 42 | 43 | def test_color(self): 44 | if environ.has_color_support(): 45 | text = Color.color("foo bar", "red") 46 | self.assertTrue("31m" in text) 47 | 48 | else: 49 | raise SkipTest("Color is not supported") 50 | 51 | def test_pout(self): 52 | """This doesn't test anything, it's just here for me to check colors""" 53 | def bar(one, two, three): 54 | pass 55 | 56 | class Foo(object): 57 | prop_str = "string value" 58 | prop_int = 123456 59 | prop_dict = { 60 | "key-1": "string dict value 1", 61 | "key-2": "string dict value 2" 62 | } 63 | 64 | d = { 65 | "bool-true": True, 66 | "bool-false": False, 67 | "float": 123456.789, 68 | "int": 1234456789, 69 | "none": None, 70 | "list": list(range(5)), 71 | "string": "foo bar che", 72 | "instance": Foo(), 73 | "class": Foo, 74 | "function": bar, 75 | } 76 | 77 | pout.v(d) 78 | 79 | -------------------------------------------------------------------------------- /docs/CUSTOMIZE.md: -------------------------------------------------------------------------------- 1 | # Customizing Pout 2 | 3 | ## How Pout decides what to print 4 | 5 | Pout uses `pout.value.Value` to decide how to print out any values passed to `pout.v()`. The `Value` class uses the `pout.value.Values` class to actually find the `Value` subclasses and decide which is the correct `Value` subclass for the given value (see the `pout.value.Values.find_class` method). 6 | 7 | You can completely replace the `Values` class with your own by doing something like: 8 | 9 | ```python 10 | import pout.value 11 | 12 | class MyValues(pout.value.Values): 13 | @classmethod 14 | def find_class(cls, val): 15 | # customize this to decide how to handle val 16 | pass 17 | 18 | pout.value.Value.values_class = MyValues 19 | ``` 20 | 21 | Pout will now use your `MyValues` class to find the correct `Value` subclass. 22 | 23 | 24 | ## Add a function 25 | 26 | This section is intended for people wanting to add core functionality to Pout itself. 27 | 28 | Add an `Interface` subclass to `pout.interface` and override any wanted methods. Usually `body_value` will be enough to do what you want, but you can change the functionality quite a bit by overriding other methods. 29 | 30 | ```python 31 | class Foo(Interface): 32 | def __call__(self, *args, **kwargs): 33 | # the args and kwargs are what's passed to pout.foo() 34 | return super().__call__(*args, **kwargs) 35 | 36 | def body_value(self, body, **kwargs): 37 | # return the value you want Foo to return 38 | pass 39 | ``` 40 | 41 | If your class is not in the `pout.interface` module you will need to manually inject your new class into pout: 42 | 43 | ```python 44 | Foo.inject() 45 | ``` 46 | 47 | After `.inject()` is called then `pout.` should work and use your custom interface subclass. 48 | 49 | 50 | ## Add a new value 51 | 52 | This section is intended for people wanting to add core functionality to Pout itself. 53 | 54 | Add a `Value` subclass to `pout.value`: 55 | 56 | ```python 57 | class CustomValue(Value): 58 | @classmethod 59 | def is_valid(cls, val): 60 | # return True if val is the right type 61 | 62 | def string_value(self): 63 | # return string representation of val 64 | ``` 65 | 66 | You can use the class hierarchy to decide when your `CustomValue` class should be checked. For example, if you want your class to be checked before `ListValue` because your custom value is a derivative of a `list` then you would just have `CustomValue` extend `ListValue` and it will be checked before `ListValue` in `Values.find_class()`. 67 | -------------------------------------------------------------------------------- /pout/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | prints out variables and other handy things to help with debugging 4 | 5 | print was too hard to read, pprint wasn't much better. I was getting sick of 6 | typing: print "var name: {}".format(var). This tries to print out variables 7 | with their name, and where the print statement was called (so you can easily 8 | find it and delete it later). 9 | 10 | link -- http://stackoverflow.com/questions/3229419/pretty-printing-nested-dictionaries-in-python 11 | link -- http://docs.python.org/library/pprint.html 12 | link -- http://docs.python.org/library/inspect.html 13 | link -- http://www.doughellmann.com/PyMOTW/inspect/ 14 | 15 | should take a look at this in more detail (repr in py2, reprlib in py3): 16 | link -- http://docs.python.org/2.7/library/repr.html 17 | 18 | since -- 6-26-12 19 | """ 20 | import sys 21 | import logging 22 | import functools 23 | 24 | from . import environ 25 | #from .compat import * 26 | from .utils import StderrStream 27 | from .reflect import Call, Reflect 28 | from .interface import Interface 29 | from .value import Value 30 | 31 | 32 | __version__ = '3.2.0' 33 | 34 | 35 | # This is the standard logger for debugging pout itself, if it hasn't been 36 | # messed with we will set it to warning so it won't print anything out 37 | logger = logging.getLogger(__name__) 38 | 39 | # don't try and configure the logger for default if it has been configured 40 | # elsewhere 41 | # http://stackoverflow.com/questions/6333916/python-logging-ensure-a-handler-is-added-only-once 42 | if len(logger.handlers) == 0: 43 | # set to True to turn on all logging: 44 | if environ.DEBUG: 45 | logger.setLevel(logging.DEBUG) 46 | log_handler = logging.StreamHandler(stream=sys.stderr) 47 | log_handler.setFormatter( 48 | logging.Formatter('[%(levelname).1s] %(message)s') 49 | ) 50 | logger.addHandler(log_handler) 51 | 52 | else: 53 | logger.setLevel(logging.WARNING) 54 | logger.addHandler(logging.NullHandler()) 55 | 56 | 57 | # this is the pout printing logger, you can modify the logger this instance 58 | # uses or completely replace it to customize functionality 59 | stream = StderrStream() 60 | 61 | 62 | def __getattr__(name): 63 | """This uses Interface.classes to match a function call on pout to an 64 | Interface class 65 | 66 | If you're wondering where the `pout.v()` function is, look at the 67 | `pout.interface.V` class 68 | 69 | :param name: str, the pout function name (eg, `pout.v`) that will match 70 | to an Interface class (eg, `pout.v` will match to `pout.interface.V`) 71 | :returns: callable 72 | """ 73 | interface_class = Interface.classes[name] 74 | module = interface_class.get_module() 75 | 76 | func = functools.partial( 77 | interface_class.create_instance, 78 | pout_module=module, 79 | pout_function_name=name, 80 | pout_interface_class=interface_class, 81 | ) 82 | func.__name__ = name 83 | func.__module__ = module 84 | return func 85 | 86 | 87 | def inject(): 88 | """Injects pout into the builtins module so it can be called from anywhere 89 | without having to be explicitly imported, this is really just for 90 | convenience when debugging 91 | 92 | https://stackoverflow.com/questions/142545/python-how-to-make-a-cross-module-variable 93 | """ 94 | try: 95 | from .compat import builtins 96 | 97 | module = sys.modules[__name__] 98 | setattr(builtins, __name__, module) 99 | #builtins.pout = pout 100 | 101 | except ImportError: 102 | pass 103 | 104 | 105 | -------------------------------------------------------------------------------- /pout/path.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import logging 4 | import sys 5 | import site 6 | import inspect 7 | 8 | from .compat import String 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Path(String): 15 | """Returns a path string relative to the current working directory (if 16 | applicable)""" 17 | def __new__(cls, path): 18 | path = os.path.abspath(path) 19 | cwd = os.getcwd() 20 | if path.startswith(cwd): 21 | path = path.replace(cwd, "", 1).lstrip(os.sep) 22 | return super().__new__(cls, path) 23 | 24 | 25 | class SitePackagesDir(String): 26 | """Finds the site-packages directory and sets the value of this string to that 27 | path""" 28 | def __new__(cls): 29 | basepath = "" 30 | try: 31 | paths = site.getsitepackages() 32 | basepath = paths[0] 33 | logger.debug( 34 | "Found site-packages directory {} using site.getsitepackages".format( 35 | basepath 36 | ) 37 | ) 38 | 39 | except AttributeError: 40 | # we are probably running this in a virtualenv, so let's try a different 41 | # approach 42 | # try and brute-force discover it since it's not defined where it 43 | # should be defined 44 | sitepath = os.path.join(os.path.dirname(site.__file__), "site-packages") 45 | if os.path.isdir(sitepath): 46 | basepath = sitepath 47 | logger.debug( 48 | "Found site-packages directory {} using site.__file__".format( 49 | basepath 50 | ) 51 | ) 52 | 53 | else: 54 | for path in sys.path: 55 | if path.endswith("site-packages"): 56 | basepath = path 57 | logger.debug( 58 | "Found site-packages directory {} using sys.path".format( 59 | basepath 60 | ) 61 | ) 62 | break 63 | 64 | if not basepath: 65 | for path in sys.path: 66 | if path.endswith("dist-packages"): 67 | basepath = path 68 | logger.debug( 69 | "Found dist-packages directory {} using sys.path".format( 70 | basepath 71 | ) 72 | ) 73 | break 74 | 75 | if not basepath: 76 | raise IOError("Could not find site-packages directory") 77 | 78 | return super(SitePackagesDir, cls).__new__(cls, basepath) 79 | 80 | 81 | class SiteCustomizeFile(String): 82 | """sets the value of the string to the sitecustomize.py file, and adds handy 83 | helper functions to manipulate it""" 84 | @property 85 | def body(self): 86 | if not self.exists(): 87 | return "" 88 | 89 | with open(self, mode="r") as fp: 90 | return fp.read() 91 | 92 | def __new__(cls): 93 | filepath = "" 94 | if "sitecustomize" in sys.modules: 95 | filepath = ModuleFile("sitecustomize") 96 | 97 | if not filepath: 98 | basepath = SitePackagesDir() 99 | filepath = os.path.join(basepath, "sitecustomize.py") 100 | 101 | instance = super(SiteCustomizeFile, cls).__new__(cls, filepath) 102 | return instance 103 | 104 | def inject(self): 105 | """inject code into sitecustomize.py that will inject pout into the builtins 106 | so it will be available globally""" 107 | if self.is_injected(): 108 | return False 109 | 110 | with open(self, mode="a+") as fp: 111 | fp.seek(0) 112 | fp.write("\n".join([ 113 | "", 114 | "try:", 115 | " import pout", 116 | "except ImportError:", 117 | " pass", 118 | "else:", 119 | " pout.inject()", 120 | "", 121 | ])) 122 | 123 | return True 124 | 125 | def exists(self): 126 | return os.path.isfile(self) 127 | 128 | def is_injected(self): 129 | body = self.body 130 | return "import pout" in body 131 | 132 | 133 | class ModuleFile(String): 134 | """Given a module name (eg, foo) find the source file that corresponds to the 135 | module, will be an empty string if modname's filepath can't be found""" 136 | def __new__(cls, modname): 137 | if isinstance(modname, String): 138 | mod = sys.modules[modname] 139 | else: 140 | mod = modname 141 | modname = mod.__name__ 142 | 143 | try: 144 | # http://stackoverflow.com/questions/6761337/inspect-getfile-vs-inspect-getsourcefile 145 | # first try and get the actual source file 146 | filepath = inspect.getsourcefile(mod) 147 | if not filepath: 148 | # get the raw file since val doesn't have a source file (could be a .pyc or .so file) 149 | filepath = inspect.getfile(mod) 150 | 151 | if filepath: 152 | path = os.path.realpath(filepath) 153 | 154 | # !!! I have doubts this if block is needed 155 | if filepath and not filepath.endswith(".py"): 156 | filepath = "" 157 | for path in sys.path: 158 | p = os.path.join(path, "{}.py".format(modname)) 159 | if os.path.isfile(p): 160 | filepath = p 161 | break 162 | 163 | except TypeError as e: 164 | filepath = "" 165 | 166 | return super(ModuleFile, cls).__new__(cls, filepath) 167 | 168 | 169 | -------------------------------------------------------------------------------- /pout/environ.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import codecs 4 | import uuid 5 | import sys 6 | 7 | from .compat import * 8 | 9 | 10 | DEBUG = bool(int(os.environ.get("POUT_DEBUG", 0))) 11 | """Set this to turn on debug output that is more verbose than pout's normal 12 | output, this is handy when trying to figure out what pout is doing and why it is 13 | failing, mainly handy for writing tests and adding functionality, normally 14 | pout's logger is the NullHandler 15 | """ 16 | 17 | 18 | ENCODING = os.environ.get("POUT_ENCODING", "UTF-8") 19 | """The encoding pout will use internally""" 20 | 21 | 22 | ENCODING_REPLACE_METHOD = os.environ.get( 23 | "POUT_ENCODING_REPLACE_METHOD", 24 | "pout.replace.{}".format(uuid.uuid4().hex) # unique value so we can lookup 25 | ) 26 | """The method to replace bad unicode characters, normally you shouldn't have to 27 | mess with this""" 28 | 29 | 30 | # https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character 31 | ENCODING_REPLACE_CHAR = String(os.environ.get( 32 | "POUT_ENCODING_REPLACE_CHAR", 33 | String("\uFFFD") 34 | )) 35 | """The character used to replace bad unicode characters, I previously used the 36 | period but figured I should use the actual replacement character""" 37 | 38 | 39 | SHOW_SIMPLE_PREFIX = bool(int(os.environ.get( 40 | "POUT_SHOW_SIMPLE_PREFIX", 41 | False 42 | ))) 43 | """This flips SHOW_INSTANCE_ID and SHOW_INSTANCE_TYPE to its value""" 44 | 45 | 46 | SHOW_SIMPLE_VALUE = bool(int(os.environ.get("POUT_SHOW_SIMPLE_VALUE", True))) 47 | """This displays simple values for Value subclasses that support it 48 | 49 | This has to be specifically supported by a Value subclass to have any effect 50 | 51 | see: https://github.com/Jaymon/pout/issues/95 52 | """ 53 | 54 | 55 | SHOW_COLOR = bool(int(os.environ.get("POUT_SHOW_COLOR", True))) 56 | """Set to True for pout to use color if the terminal supports it""" 57 | 58 | 59 | OBJECT_DEPTH = int(os.environ.get("POUT_OBJECT_DEPTH", 5)) 60 | """Change this to set how far down in depth pout will print instances with full 61 | ObjectValue output while it is compiling the value for the passed in instance. 62 | 63 | some objects have many layers of nested objects which makes their pout.v() 64 | output annoying, but setting this to like 1 would cause all those nested 65 | instances to just have repr(instance) be printed instead""" 66 | 67 | 68 | OBJECT_STRING_LIMIT = int(os.environ.get("POUT_OBJECT_STR_LIMIT", 500)) 69 | """Limits the length of an object's __str__() method output""" 70 | 71 | 72 | ITERATE_LIMIT = int(os.environ.get( 73 | "POUT_ITERATE_LIMIT", 74 | os.environ.get("POUT_ITERATOR_LIMIT", 101) 75 | )) 76 | """Change this to limit how many rows of list/set/etc and how many keys of dict 77 | you want to print out. Turns out, after so many it becomes a pain to actually 78 | inspect the object""" 79 | 80 | 81 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\t") 82 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", " ") 83 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "‧ ") # \u2027 84 | # https://www.reddit.com/r/Unicode/comments/1cd05m9/smallest_shortest_thinnest_widest_tallest_unicode/ 85 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u05B4 ") 86 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u05BC ") 87 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "ᐧ ") 88 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u0387 ") 89 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u05C5 ") 90 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\uFE52 ") 91 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u0F0B ") 92 | # https://www.compart.com/en/unicode/U+0F0C 93 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u0F0C ") 94 | # https://www.compart.com/en/unicode/U+115C5 95 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\U000115C5 ") 96 | 97 | # my favorite dot 98 | # https://www.compart.com/en/unicode/U+0701 99 | INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u0701 ") 100 | # my favorite vertical line 101 | # https://en.wikipedia.org/wiki/Box_Drawing 102 | #INDENT_STRING = os.environ.get("POUT_INDENT_STRING", "\u2502 ") 103 | """This is what pout uses to indent when it is creating the output 104 | 105 | You can check here for other delimiters: 106 | 107 | https://www.compart.com/en/unicode/category/Po 108 | """ 109 | 110 | 111 | KEY_QUOTE_CHAR = os.environ.get("POUT_KEY_QUOTE_CHAR", "\'") 112 | """pout will use this quotation character to wrap dict keys""" 113 | 114 | # we do some tricksy normalizing here for environments where it is 115 | # hard to set the actual quote value in a variable, I was getting a 116 | # bunch of errors like: "unterminated quote" when trying to just set 117 | # a quote value in my environment 118 | if KEY_QUOTE_CHAR == "DOUBLE": 119 | KEY_QUOTE_CHAR = "\"" 120 | 121 | elif KEY_QUOTE_CHAR == "SINGLE": 122 | KEY_QUOTE_CHAR = "'" 123 | 124 | 125 | def handle_decode_replace(e): 126 | """this handles replacing bad characters when printing out 127 | 128 | http://www.programcreek.com/python/example/3643/codecs.register_error 129 | http://bioportal.weizmann.ac.il/course/python/PyMOTW/PyMOTW/docs/codecs/index.html 130 | https://pymotw.com/2/codecs/ 131 | """ 132 | count = e.end - e.start 133 | #return "." * count, e.end 134 | 135 | global ENCODING_REPLACE_CHAR 136 | return ENCODING_REPLACE_CHAR * count, e.end 137 | 138 | 139 | # register our decode replace method when encoding 140 | try: 141 | codecs.lookup_error(ENCODING_REPLACE_METHOD) 142 | 143 | except LookupError: 144 | codecs.register_error(ENCODING_REPLACE_METHOD, handle_decode_replace) 145 | 146 | else: 147 | raise ValueError( 148 | "{} has already been registered".format(ENCODING_REPLACE_METHOD) 149 | ) 150 | 151 | 152 | def has_color_support(): 153 | """True if the environment has color support 154 | 155 | This is a simplified version of: 156 | https://github.com/django/django/blob/main/django/core/management/color.py 157 | 158 | See: 159 | - https://stackoverflow.com/questions/ 160 | - https://stackoverflow.com/questions/7445658/ 161 | 162 | :returns: bool, True if the environment supports color 163 | """ 164 | global SHOW_COLOR 165 | 166 | return ( 167 | SHOW_COLOR 168 | and hasattr(sys.stdout, "isatty") 169 | and sys.stdout.isatty() 170 | and ( 171 | sys.platform != "win32" 172 | or "ANSICON" in os.environ 173 | or 174 | # Windows Terminal supports VT codes. 175 | "WT_SESSION" in os.environ 176 | or 177 | # Microsoft Visual Studio Code's built-in terminal supports colors. 178 | os.environ.get("TERM_PROGRAM") == "vscode" 179 | ) 180 | ) 181 | 182 | -------------------------------------------------------------------------------- /pout/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals, division, print_function, absolute_import 3 | 4 | import sys 5 | import os 6 | import argparse 7 | import logging 8 | import platform 9 | 10 | import pout 11 | from pout.path import SitePackagesDir, SiteCustomizeFile 12 | from pout.utils import String 13 | 14 | 15 | level = logging.INFO 16 | logging.basicConfig(format="%(message)s", level=level, stream=sys.stdout) 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class Input(String): 21 | """On the command line you can pass in a file or you can pipe stdin, this 22 | class normalizes whichever to just the proper thing we want""" 23 | def __new__(cls, val): 24 | if val: 25 | if os.path.isfile(val): 26 | path = val 27 | with open(path, mode="rb") as fp: 28 | val = fp.read() 29 | 30 | else: 31 | # http://stackoverflow.com/questions/8478137/how-redirect-a-shell-command-output-to-a-python-script-input 32 | val = sys.stdin.read() 33 | 34 | return super(Input, cls).__new__(cls, val) 35 | 36 | 37 | def main_json(args): 38 | """ 39 | mapped to pout.j on command line, use in a pipe 40 | 41 | since -- 2013-9-10 42 | 43 | $ command-that-outputs-json | pout.json 44 | """ 45 | s = Input(args.input) 46 | pout.j(s) 47 | #data = sys.stdin.readlines() 48 | #pout.j(os.sep.join(data)) 49 | return 0 50 | 51 | 52 | def main_char(args): 53 | """ 54 | mapped to pout.c on the command line, use in a pipe 55 | since -- 2013-9-10 56 | 57 | $ echo "some string I want to see char values for" | pout.char 58 | """ 59 | s = Input(args.input) 60 | pout.c(s) 61 | #data = sys.stdin.readlines() 62 | #pout.c(os.sep.join(data)) 63 | return 0 64 | 65 | 66 | def main_inject(args): 67 | """ 68 | mapped to pout.inject on the command line, makes it easy to make pout global 69 | without having to actually import it in your python environment 70 | 71 | .. since:: 2018-08-13 72 | 73 | :param args: Namespace, the parsed CLI arguments passed into the application 74 | :returns: int, the return code of the CLI 75 | """ 76 | ret = 0 77 | 78 | try: 79 | filepath = SiteCustomizeFile() 80 | if filepath.is_injected(): 81 | logger.info("Pout has already been injected into {}".format(filepath)) 82 | 83 | else: 84 | if filepath.inject(): 85 | logger.info("Injected pout into {}".format(filepath)) 86 | else: 87 | logger.info("Failed to inject pout into {}".format(filepath)) 88 | 89 | except IOError as e: 90 | ret = 1 91 | logger.info(str(e)) 92 | 93 | return ret 94 | 95 | 96 | def main_info(args): 97 | """Just prints out info about the pout installation 98 | 99 | .. since:: 2018-08-20 100 | 101 | :param args: Namespace, the parsed CLI arguments passed into the application 102 | :returns: int, the return code of the CLI 103 | """ 104 | if args.site_packages: 105 | logger.info(SitePackagesDir()) 106 | 107 | else: 108 | logger.info("Python executable: {}".format(sys.executable)) 109 | logger.info("Python version: {}".format(platform.python_version())) 110 | logger.info("Python site-packages: {}".format(SitePackagesDir())) 111 | logger.info("Python sitecustomize: {}".format(SiteCustomizeFile())) 112 | # https://stackoverflow.com/questions/4152963/get-the-name-of-current-script-with-python 113 | #logger.info("Pout executable: {}".format(subprocess.check_output(["which", "pout"]))) 114 | logger.info("Pout executable: {}".format(os.path.abspath(os.path.expanduser(str(sys.argv[0]))))) 115 | logger.info("Pout version: {}".format(pout.__version__)) 116 | 117 | filepath = SiteCustomizeFile() 118 | logger.info("Pout injected: {}".format(filepath.is_injected())) 119 | 120 | 121 | def main(): 122 | #parser = argparse.ArgumentParser(description='Pout CLI', conflict_handler="resolve") 123 | parser = argparse.ArgumentParser(description='Pout CLI') 124 | parser.add_argument("--version", "-V", action='version', version="%(prog)s {}".format(pout.__version__)) 125 | parser.add_argument("--debug", "-d", action="store_true", help="More verbose logging") 126 | 127 | # some parsers can take an input string, this is the common argument for them 128 | common_parser = argparse.ArgumentParser(add_help=False) 129 | common_parser.add_argument("--debug", "-d", action="store_true", help="More verbose output") 130 | 131 | input_parser = argparse.ArgumentParser(add_help=False) 132 | input_parser.add_argument('input', nargs="?", default=None, help="the input file, value, or pipe") 133 | 134 | subparsers = parser.add_subparsers(dest="command", help="a sub command") 135 | subparsers.required = True # https://bugs.python.org/issue9253#msg186387 136 | 137 | # $ pout inject 138 | desc = "Inject pout into python builtins so it doesn't need to be imported" 139 | subparser = subparsers.add_parser( 140 | "inject", 141 | parents=[common_parser], 142 | help=desc, 143 | description=desc, 144 | #add_help=False 145 | conflict_handler="resolve", 146 | ) 147 | subparser.set_defaults(func=main_inject) 148 | 149 | # $ pout char 150 | desc = "Dump all the character information of each character in a string, pout.c() on the command line" 151 | subparser = subparsers.add_parser( 152 | "char", 153 | parents=[common_parser, input_parser], 154 | help=desc, 155 | description=desc, 156 | #add_help=False 157 | conflict_handler="resolve", 158 | ) 159 | subparser.set_defaults(func=main_char) 160 | 161 | # $ pout json 162 | desc = "Pretty print json, pout.j() on the command line" 163 | subparser = subparsers.add_parser( 164 | "json", 165 | parents=[common_parser, input_parser], 166 | help=desc, 167 | description=desc, 168 | #add_help=False 169 | conflict_handler="resolve", 170 | ) 171 | subparser.set_defaults(func=main_json) 172 | 173 | # $ pout info 174 | desc = "Print pout and python information" 175 | subparser = subparsers.add_parser( 176 | "info", 177 | parents=[common_parser], 178 | help=desc, 179 | description=desc, 180 | #add_help=False 181 | conflict_handler="resolve", 182 | ) 183 | subparser.add_argument( 184 | "--site-packages", "-s", 185 | dest="site_packages", 186 | action="store_true", 187 | help="just print the site-packages directory and nothing else", 188 | ) 189 | subparser.set_defaults(func=main_info) 190 | 191 | args = parser.parse_args() 192 | 193 | # mess with logging 194 | global level 195 | if args.debug: 196 | level = logging.DEBUG 197 | logger.setLevel(level) 198 | 199 | code = args.func(args) 200 | sys.exit(code) 201 | 202 | 203 | if __name__ == "__main__": 204 | main() 205 | 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pout 2 | 3 | A collection of handy functions for printing out variables and debugging Python code. 4 | 5 | [print](https://docs.python.org/3/library/functions.html#print) didn't give enough information while debugging, [pprint](https://docs.python.org/3/library/pprint.html) wasn't much better. I was also getting sick of typing things like: `print("var = ", var)`. 6 | 7 | Pout tries to print out variables with their name, and for good measure, it also prints where the `pout` function was called so you can easily find it and delete it when you're done debugging. 8 | 9 | I use pout extensively in basically every python project I work on. 10 | 11 | 12 | ## Methods 13 | 14 | ### pout.v(arg1, [arg2, ...]) -- easy way to print variables 15 | 16 | example 17 | 18 | ```python 19 | foo = 1 20 | pout.v(foo) 21 | 22 | bar = [1, 2, [3, 4], 5] 23 | pout.v(bar) 24 | ``` 25 | 26 | should print something like: 27 | 28 | foo = 1 29 | (/file.py:line) 30 | 31 | bar (4) = 32 | [ 33 | 0: 1, 34 | 1: 2, 35 | 2: 36 | [ 37 | 0: 3, 38 | 1: 4 39 | ], 40 | 3: 5 41 | ] 42 | (/file.py:line) 43 | 44 | You can send as many variables as you want into the call 45 | 46 | 47 | ```python 48 | # pass in as many variables as you want 49 | pout.v(foo, bar, che) 50 | 51 | # a multi-line call is also fine 52 | pout.v( 53 | foo, 54 | bar 55 | ) 56 | ``` 57 | 58 | 59 | ### pout.h() -- easy way to print "here" in the code 60 | 61 | example 62 | 63 | ```python 64 | pout.h(1) 65 | # do something else 66 | pout.h(2) 67 | 68 | # do even more of something else 69 | pout.h() 70 | ``` 71 | 72 | Should print something like: 73 | 74 | here 1 (/file.py:line) 75 | 76 | here 2 (/file.py:line) 77 | 78 | here N (/file.py:N) 79 | 80 | 81 | ### pout.t() -- print a backtrace 82 | 83 | Prints a nicely formatted backtrace, by default this should compact system python calls (eg, anything in dist-packages) which makes the backtrace easier for me to follow. 84 | 85 | example: 86 | 87 | ```python 88 | pout.t() 89 | ``` 90 | 91 | should print something like: 92 | 93 | 15 - C:\Python27\lib\runpy.py:162 94 | 14 - C:\Python27\lib\runpy.py:72 95 | 13 - C:\Python27\lib\unittest\__main__.py:12 96 | 12 - C:\Python27\lib\unittest\main.py:95 97 | 11 - C:\Python27\lib\unittest\main.py:229 98 | 10 - C:\Python27\lib\unittest\runner.py:151 99 | 09 - C:\Python27\lib\unittest\suite.py:65 100 | 08 - C:\Python27\lib\unittest\suite.py:103 101 | 07 - C:\Python27\lib\unittest\suite.py:65 102 | 06 - C:\Python27\lib\unittest\suite.py:103 103 | 05 - C:\Python27\lib\unittest\suite.py:65 104 | 04 - C:\Python27\lib\unittest\suite.py:103 105 | 03 - C:\Python27\lib\unittest\case.py:376 106 | 02 - C:\Python27\lib\unittest\case.py:318 107 | 01 - C:\Projects\Pout\_pout\src\test_pout.py:50 108 | 109 | pout.t() 110 | 111 | 112 | ### pout.p([title]) -- quick and dirty profiling 113 | 114 | example 115 | 116 | ```python 117 | p("starting profile") 118 | time.sleep(1) 119 | p() # stop the "starting profile" session 120 | 121 | 122 | # you can go N levels deep 123 | p("one") 124 | p("two") 125 | time.sleep(0.5) 126 | p() # stop profiling of "two" 127 | time.sleep(0.5) 128 | p() # stop profiling of "one" 129 | 130 | 131 | # you can also use with 132 | with p("benchmarking"): 133 | time.sleep(0.5) 134 | ``` 135 | 136 | should print something like: 137 | 138 | starting profile - 1008.2 ms 139 | start: 1368137723.7 (/file/path:n) 140 | stop: 1368137724.71(/file/path:n) 141 | 142 | 143 | one > two - 509.2 ms 144 | start: 1368137722.69 (/file/path:n) 145 | stop: 1368137723.2(/file/path:n) 146 | 147 | 148 | one - 1025.9 ms 149 | start: 1368137722.68 (/file/path:n) 150 | stop: 1368137723.7(/file/path:n) 151 | 152 | 153 | ### pout.x(arg1, [arg2, ...]) -- like pout.v but then will run sys.exit(1) 154 | 155 | This just prints out where it was called from, so you can remember where you exited the code while debugging 156 | 157 | example: 158 | 159 | ```python 160 | pout.x() 161 | ``` 162 | 163 | will print something like this before exiting with an exit code of 1: 164 | 165 | ```python 166 | exit (/file/path:n) 167 | ``` 168 | 169 | 170 | ### pout.b([title[, rows[, sep]]]) -- prints lots of lines to break up output 171 | 172 | This is is handy if you are printing lots of stuff in a loop and you want to break up the output into sections. 173 | 174 | example: 175 | 176 | ```python 177 | pout.b() 178 | pout.b('this is the title') 179 | pout.b('this is the title 2', 5) 180 | pout.b('this is the title 3', 3, '=') 181 | ``` 182 | 183 | Would result in output like: 184 | 185 | ******************************************************************************** 186 | (/file/path:n) 187 | 188 | 189 | ****************************** this is the title ******************************* 190 | (/file/path:n) 191 | 192 | 193 | ******************************************************************************** 194 | ******************************************************************************** 195 | ***************************** this is the title 2 ****************************** 196 | ******************************************************************************** 197 | ******************************************************************************** 198 | (/file/path:n) 199 | 200 | 201 | ================================================================================ 202 | ============================= this is the title 3 ============================== 203 | =============================================================================== 204 | (/file/path:n) 205 | 206 | 207 | ### pout.c(str1, [str2, ...]) -- print info about each char in each str 208 | 209 | Kind of like `od -c` on the command line. 210 | 211 | example: 212 | 213 | ```python 214 | pout.c('this') 215 | ``` 216 | 217 | will print something like: 218 | 219 | Total Characters: 4 220 | t 't' \u0074 LATIN SMALL LETTER T 221 | h 'h' \u0068 LATIN SMALL LETTER H 222 | i 'i' \u0069 LATIN SMALL LETTER I 223 | s 's' \u0073 LATIN SMALL LETTER S 224 | (/file/path:n) 225 | 226 | This could fail if Python isn't compiled with 4 byte unicode support, just something to be aware of, but chances are, if you don't have 4 byte unicode supported Python, you're not doing much with 4 byte unicode. 227 | 228 | 229 | ### pout.s(arg1, [arg2, ...]) -- easy way to return pretty versions of variables 230 | 231 | Just like `pout.v()` but will return the value as a string 232 | 233 | 234 | ### pout.ss(arg1, [arg2, ...]) -- easy way to return pretty versions of variables without meta information 235 | 236 | Just like `pout.vv()` but will return the value as a string 237 | 238 | 239 | ### pout.l([logger_name, [logger_level]]) -- turn logging on just for this context 240 | 241 | Turns logging on for the given level (defaults to `logging.DEBUG`) and prints the logs to __stderr__. Useful when you just want to check the logs of something without modifying your current logging configuration. 242 | 243 | example: 244 | 245 | ```python 246 | with pout.l(): 247 | logger.debug("This will print to the screen even if logging is off") 248 | logger.debug("this will not print if logging is off") 249 | 250 | with pout.l("name"): 251 | # if "name" logger is used it will print to stderr 252 | # "name" logger goes back to previous configuration 253 | ``` 254 | 255 | ### pout.tofile([path]) 256 | 257 | Routes pout's output to a file (defaults to `./pout.txt`) 258 | 259 | example: 260 | 261 | ```python 262 | with pout.tofile(): 263 | # everything in this with block will print to a file in current directory 264 | pout.b() 265 | s = "foo" 266 | pout.v(s) 267 | 268 | pout.s() # this will print to stderr 269 | ``` 270 | 271 | 272 | ## Customizing Pout 273 | 274 | ### object magic method 275 | 276 | Any class object can define a `__pout__` magic method, similar to Python's built in `__str__` magic method that can return a customized string of the object if you want to. This method can return anything, it will be run through Pout's internal stringify methods to convert it to a string and print it out. 277 | 278 | 279 | ## Console commands 280 | 281 | ### pout json 282 | 283 | running a command on the command line that outputs a whole a bunch of json? Pout can help: 284 | 285 | $ some-command-that-outputs-json | pout json 286 | 287 | 288 | ### pout char 289 | 290 | Runs `pout.c` but on the output from a command line script: 291 | 292 | $ echo "some string with chars to analyze" | pout char 293 | 294 | 295 | ## Install 296 | 297 | Use PIP 298 | 299 | pip install pout 300 | 301 | Generally, the pypi version and the github version shouldn't be that out of sync, but just in case, you can install from github also: 302 | 303 | pip install -U "git+https://github.com/Jaymon/pout#egg=pout" 304 | 305 | 306 | ------------------------------------------------------------------------------- 307 | 308 | ## Make Pout easier to use 309 | 310 | When debugging, it's really nice not having to put `import pout` at the top of every module you want to use it in, so there's a command for that, if you put: 311 | 312 | ```python 313 | import pout 314 | pout.inject() 315 | ``` 316 | 317 | Somewhere near the top of your application startup script, then `pout` will be available in all your files whether you imported it or not, it will be just like `str`, `object`, or the rest of python's standard library. 318 | 319 | If you don't even want to bother with doing that, then just run: 320 | 321 | ``` 322 | $ pout inject 323 | ``` 324 | 325 | from the command line and it will modify your python environment to make pout available as a builtin module, just like the python standard library. This is super handy for development virtual environments. 326 | 327 | -------------------------------------------------------------------------------- /tests/reflect_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import testdata, TestCase 4 | 5 | import pout 6 | from pout.compat import * 7 | from pout.reflect import CallString, Call 8 | 9 | 10 | class ReflectTest(TestCase): 11 | def test_discovery(self): 12 | foo = 1 13 | pout.v(foo, "foo bar che") 14 | 15 | def test__get_arg_info(self): 16 | foo = 1 17 | with testdata.capture() as c: 18 | pout.v(foo) 19 | self.assertTrue('foo = ' in c) 20 | 21 | def test_multi_command_on_one_line(self): 22 | """make sure we are finding the correct call on a multi command line""" 23 | name = "foo" 24 | val = 1 25 | with testdata.capture() as c: 26 | if not hasattr(self, name): pout.v(name); hasattr(self, name) 27 | self.assertTrue("name = " in c) 28 | self.assertTrue("(3)" in c) 29 | self.assertTrue("foo" in c) 30 | 31 | 32 | class CallStringTest(TestCase): 33 | def test_string_in_parse(self): 34 | """https://github.com/Jaymon/pout/issues/45""" 35 | c = CallString('pout.v(":" in "foo:bar")') 36 | self.assertEqual('":" in "foo:bar"', c.arg_names()[0]) 37 | 38 | c = CallString('pout.v(":" in val)') 39 | self.assertEqual('":" in val', c.arg_names()[0]) 40 | 41 | c = CallString("pout.v(':' in val)") 42 | self.assertEqual("':' in val", c.arg_names()[0]) 43 | 44 | #val = set([":"]) 45 | #pout.v(":" in val) 46 | 47 | def test_is_complete_1(self): 48 | c = CallString("foo())") 49 | self.assertFalse(c.is_complete()) 50 | 51 | c = CallString("foo(bar") 52 | self.assertFalse(c.is_complete()) 53 | 54 | c = CallString('foo("".join(bar.che), func())') 55 | self.assertTrue(c.is_complete()) 56 | 57 | c = CallString("\n".join([ 58 | " pout.v(", 59 | ' "foo",', 60 | ' "bar",', 61 | ' "che",', 62 | ' )' 63 | ])) 64 | self.assertTrue(c.is_complete()) 65 | 66 | def test_is_complete_2(self): 67 | c = CallString('foo(bar, "a) something"') 68 | self.assertFalse(c.is_complete()) 69 | 70 | c = CallString('foo(bar, "a) something")') 71 | self.assertTrue(c.is_complete()) 72 | 73 | c = CallString('foo("a) something"); func()') 74 | self.assertTrue(c.is_complete()) 75 | 76 | def test_arg_names_1(self): 77 | c = CallString('pout.v(left, " ".join(FooIssue34.bar_che), right)') 78 | arg_names = c.arg_names() 79 | self.assertEqual(3, len(arg_names)) 80 | 81 | def test_arg_names_2(self): 82 | """see also VTest.test_multiline_comma()""" 83 | 84 | # NOTE 12-21-2018 this used to pass with the old parsing code (it's 85 | # invalid) but using the tokenize module means this no longer works 86 | r = CallString("\n".join([ 87 | " pout.v(", 88 | '"foo",', 89 | '"bar",', 90 | '"che"' 91 | ])).arg_names() 92 | self.assertEqual(0, len(r)) 93 | # for x in range(3): 94 | # self.assertEqual("", r[x]) 95 | 96 | r = CallString("\n".join([ 97 | " pout.v(", 98 | ' "foo",', 99 | ' "bar",', 100 | ' "che",', 101 | ' )' 102 | ])).arg_names() 103 | self.assertEqual(3, len(r)) 104 | for x in range(3): 105 | self.assertEqual("", r[x]) 106 | 107 | r = CallString(" pout.v(\"this string has 'mixed quotes\\\"\")").arg_names() 108 | self.assertEqual(1, len(r)) 109 | self.assertEqual("", r[0]) 110 | 111 | r = CallString(" pout.v(name); hasattr(self, name)").arg_names() 112 | self.assertEqual("name", r[0]) 113 | self.assertEqual(1, len(r)) 114 | 115 | r = CallString("\n".join([ 116 | " pout.v(", 117 | '"foo",', 118 | '"bar",', 119 | '"che",', 120 | ")" 121 | ])).arg_names() 122 | for x in range(3): 123 | self.assertEqual("", r[x]) 124 | 125 | tests = { 126 | "pout.v(foo, [bar, che])": ["foo", "[bar, che]"], 127 | "pout.v(foo, bar)": ["foo", "bar"], 128 | "pout.v(foo)": ["foo"], 129 | "pout.v('foo')": [""], 130 | 'pout.v("foo")': [""], 131 | "pout.v('foo\'bar')": [""], 132 | "pout.v('foo, bar, che')": [""], 133 | "pout.v((foo, bar, che))": ["(foo, bar, che)"], 134 | "pout.v((foo, (bar, che)))": ["(foo, (bar, che))"], 135 | "pout.v([foo, bar, che])": ["[foo, bar, che]"], 136 | "pout.v([foo, [bar, che]])": ["[foo, [bar, che]]"], 137 | "pout.v([[foo], [bar, che]])": ["[[foo], [bar, che]]"], 138 | } 139 | for inp, outp in tests.items(): 140 | r = CallString(inp).arg_names() 141 | for i, expected in enumerate(r): 142 | self.assertEqual(expected, r[i]) 143 | 144 | def test_multi_args(self): 145 | ''' 146 | since -- 6-30-12 147 | 148 | this actually tests _get_arg_names 149 | ''' 150 | 151 | foo = 1 152 | bar = 2 153 | che = {'foo': 3, 'bar': 4} 154 | 155 | def func(a, b): 156 | return a + b 157 | 158 | 159 | with testdata.capture() as c: 160 | pout.v("this string has 'mixed quotes\"") 161 | self.assertTrue("this string has 'mixed quotes\"" in c) 162 | 163 | with testdata.capture() as c: 164 | pout.v('this string has \'mixed quotes"') 165 | self.assertTrue('this string has \'mixed quotes"' in c) 166 | 167 | with testdata.capture() as c: 168 | pout.v('this string has \'single quotes\' and "double quotes"') 169 | self.assertTrue('this string has \'single quotes\' and "double quotes"' in c) 170 | 171 | with testdata.capture() as c: 172 | pout.v(foo, 'this isn\'t a string, just kidding') 173 | self.assertTrue("foo" in c) 174 | self.assertTrue('this isn\'t a string, just kidding' in c) 175 | 176 | with testdata.capture() as c: 177 | pout.v('this string is formatted {} {}'.format(foo, bar)) 178 | self.assertTrue('this string is formatted 1 2' in c) 179 | 180 | with testdata.capture() as c: 181 | pout.v('this string' + " is added together") 182 | self.assertTrue("this string is added together" in c) 183 | 184 | with testdata.capture() as c: 185 | pout.v(func('this string', " has 'single quotes'")) 186 | self.assertTrue("this string has 'single quotes'" in c) 187 | 188 | with testdata.capture() as c: 189 | pout.v('this string has \'single quotes\'') 190 | self.assertTrue("this string has 'single quotes'" in c) 191 | 192 | with testdata.capture() as c: 193 | pout.v("this string has \"quotes\"") 194 | self.assertTrue("this string has \"quotes\"" in c) 195 | 196 | with testdata.capture() as c: 197 | pout.v(che['foo'], che['bar']) 198 | self.assertTrue("che['foo']" in c) 199 | self.assertTrue("che['bar']" in c) 200 | 201 | with testdata.capture() as c: 202 | pout.v(foo, "this isn't a string, just kidding") 203 | self.assertTrue("foo" in c) 204 | self.assertTrue('this isn\'t a string, just kidding' in c) 205 | 206 | with testdata.capture() as c: 207 | pout.v(foo, "(a) this is a string") 208 | self.assertTrue("foo" in c) 209 | self.assertTrue("(a) this is a string" in c) 210 | 211 | with testdata.capture() as c: 212 | pout.v(foo, "(a this is a string") 213 | self.assertTrue("foo" in c) 214 | self.assertTrue("(a this is a string" in c) 215 | 216 | with testdata.capture() as c: 217 | pout.v(foo, "a) this is a string") 218 | self.assertTrue("foo" in c) 219 | self.assertTrue("a) this is a string" in c) 220 | 221 | with testdata.capture() as c: 222 | pout.v(foo, "this is a, string") 223 | self.assertTrue("foo" in c) 224 | self.assertTrue("this is a, string" in c) 225 | 226 | with testdata.capture() as c: 227 | pout.v(foo, "this is a simple string") 228 | self.assertTrue("foo" in c) 229 | self.assertTrue("this is a simple string" in c) 230 | 231 | with testdata.capture() as c: 232 | pout.v(foo, bar, func(1, 2)) 233 | self.assertTrue("foo" in c) 234 | self.assertTrue("bar" in c) 235 | self.assertTrue("func(1, 2)" in c) 236 | 237 | def test_string_arg(self): 238 | #self.skipTest("I was using this to debug a lot of the above tests") 239 | foo = 1 240 | bar = 2 241 | che = {'foo': 3, 'bar': 4} 242 | 243 | def func(a, b): 244 | return a + b 245 | 246 | pout.v(type([])) 247 | return 248 | 249 | pout.v(func(1, 2)) 250 | return 251 | 252 | pout.v(func('this string', " has 'single quotes'")) 253 | return 254 | 255 | pout.v(foo, 'this isn\'t a string, just kidding') 256 | return 257 | 258 | pout.v(che['foo'], che['bar']) 259 | return 260 | 261 | pout.v(che['foo']) 262 | return 263 | 264 | pout.v('isn\'t, no') 265 | return 266 | 267 | pout.v(foo, "a) this is a string") 268 | return 269 | 270 | pout.v("foo bar"); print("foo") 271 | return 272 | 273 | 274 | c = CallString("pout.v('isn\'t, no')") 275 | pout2.v(c.arg_names()) 276 | 277 | 278 | class CallTest(TestCase): 279 | def get_caller_frame_info(self, lines, **kwargs): 280 | path = self.create_file(lines) 281 | kwargs.setdefault("lineno", 1) 282 | 283 | return self.mock( 284 | filename=path, 285 | code_context=[lines[kwargs["lineno"] - 1]], 286 | index=0, 287 | **kwargs 288 | ) 289 | 290 | def test_find_call_info_one_line(self): 291 | fi = self.get_caller_frame_info([ 292 | "pout.v(1, 2, 3, 4, 5)" 293 | ]) 294 | ci = Call.find_callstring_info("pout", "v", fi) 295 | self.assertEqual(["1", "2", "3", "4", "5"], ci["arg_names"]) 296 | 297 | def test_find_call_info_multi_line(self): 298 | fi = self.get_caller_frame_info([ 299 | "pout.v(", 300 | " 1,", 301 | " 2,", 302 | ")", 303 | ]) 304 | ci = Call.find_callstring_info("pout", "v", fi) 305 | self.assertEqual(["1", "2"], ci["arg_names"]) 306 | 307 | def test_find_call_info_callback_1(self): 308 | fi = self.get_caller_frame_info( 309 | [ 310 | "callback = pout.v", 311 | "callback(", 312 | " 1,", 313 | " 2,", 314 | ")", 315 | ], 316 | lineno=2 317 | ) 318 | ci = Call.find_callstring_info("pout", "v", fi) 319 | self.assertEqual(["1", "2"], ci["arg_names"]) 320 | 321 | def test_find_call_info_callback_2(self): 322 | fi = self.get_caller_frame_info( 323 | [ 324 | "d['v callback'] = pout.v", 325 | "d['v callback'](", 326 | " 1,", 327 | " 2,", 328 | ")", 329 | ], 330 | lineno=2 331 | ) 332 | ci = Call.find_callstring_info("pout", "v", fi) 333 | self.assertEqual(["1", "2"], ci["arg_names"]) 334 | 335 | def test_find_call_info_semicolon_1(self): 336 | fi = self.get_caller_frame_info([ 337 | "pout.b(3); pout.v(1, 2); pout.b()" 338 | ]) 339 | ci = Call.find_callstring_info("pout", "v", fi) 340 | self.assertEqual(["1", "2"], ci["arg_names"]) 341 | 342 | def test_find_call_info_semicolon_2(self): 343 | """This test doesn't resolve because I think it's so rare as to not be 344 | worth the time to make it work""" 345 | fi = self.get_caller_frame_info( 346 | [ 347 | "callback = pout.v", 348 | "pout.b(3); callback(1, 2); pout.b()" 349 | ], 350 | lineno=2 351 | ) 352 | ci = Call.find_callstring_info("pout", "v", fi) 353 | self.assertEqual([], ci["arg_names"]) 354 | 355 | -------------------------------------------------------------------------------- /pout/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import logging 4 | import re 5 | import textwrap 6 | 7 | from .compat import String as BaseString, Bytes 8 | from . import environ 9 | 10 | 11 | class String(BaseString): 12 | """Small wrapper around string/unicode that guarantees output will be a real 13 | string ("" in py3 and u"" in py2) and won't fail with a UnicodeException""" 14 | 15 | types = (BaseString,) 16 | 17 | def __new__(cls, arg): 18 | """make sure arg is a unicode string 19 | 20 | :param arg: mixed, arg can be anything 21 | :returns: unicode, a u"" string will always be returned 22 | """ 23 | try: 24 | if isinstance(arg, Bytes): 25 | arg = arg.decode( 26 | environ.ENCODING, 27 | errors=environ.ENCODING_REPLACE_METHOD 28 | ) 29 | 30 | else: 31 | if not isinstance(arg, BaseString): 32 | try: 33 | arg = BaseString(arg) 34 | except TypeError: 35 | # this error can happen if arg.__str__() doesn't return 36 | # a string, so we call the method directly and go back 37 | # through the __new__() flow 38 | if hasattr(arg, "__str__"): 39 | arg = cls(arg.__str__()) 40 | 41 | except RuntimeError as e: 42 | arg = e 43 | 44 | return super().__new__(cls, arg) 45 | 46 | def truncate(self, size, postfix="", sep=None): # copied from datatypes 47 | """similar to a normal string slice but it actually will split on a word 48 | boundary 49 | 50 | :Example: 51 | s = "foo barche" 52 | print s[0:5] # "foo b" 53 | s2 = String(s) 54 | print s2.truncate(5) # "foo" 55 | 56 | truncate a string by word breaks instead of just length 57 | this will guarantee that the string is not longer than length, but it 58 | could be shorter 59 | 60 | * http://stackoverflow.com/questions/250357/smart-truncate-in-python/250373#250373 61 | * This was originally a method called word_truncate by Cahlan Sharp for 62 | Undrip. 63 | * There is also a Plancast Formatting.php substr method that does 64 | something similar 65 | 66 | :param size: int, the size you want to truncate to at max 67 | :param postfix: str, what you would like to be appended to the 68 | truncated string 69 | :param sep: str, by default, whitespace is used to decide where to 70 | truncate the string, but if you pass in something for sep then that 71 | will be used to truncate instead 72 | :returns: str, a new string, truncated 73 | """ 74 | if len(self) < size: 75 | return self 76 | 77 | # our algo is pretty easy here, it truncates the string to size - 78 | # postfix size then right splits the string on any whitespace for a 79 | # maximum of one time and returns the first item of that split right 80 | # stripped of whitespace (just in case) 81 | postfix = type(self)(postfix) 82 | ret = self[0:size - len(postfix)] 83 | # if rsplit sep is None, any whitespace string is a separator 84 | ret = ret[:-1].rsplit(sep, 1)[0].rstrip() 85 | return type(self)(ret + postfix) 86 | 87 | def indent(self, indent, count=1): # copied from datatypes 88 | """add whitespace to the beginning of each line of val 89 | 90 | http://code.activestate.com/recipes/66055-changing-the-indentation-of-a-multi-line-string/ 91 | 92 | :param indent: str, what you want the prefix of each line to be 93 | :param count: int, how many times to apply indent to each line 94 | :returns: str, string with prefix at the beginning of each line 95 | """ 96 | if not indent: 97 | return self 98 | 99 | s = ((indent * count) + line for line in self.splitlines(True)) 100 | s = "".join(s) 101 | return type(self)(s) 102 | 103 | def dedent(self): 104 | """Dedent common whitespace from the string 105 | 106 | https://docs.python.org/3/library/textwrap.html 107 | 108 | :returns: str, a new string instance with removed common whitespace 109 | """ 110 | return type(self)(textwrap.dedent(self)) 111 | 112 | def camelcase(self): 113 | """Convert a string to use camel case (spaces removed and capital 114 | letters)""" 115 | return "".join(map(lambda s: s.title(), re.split(r"[_-]+", self))) 116 | 117 | def snakecase(self): 118 | """Convert a string to use snake case (lowercase with underscores in 119 | place of spaces)""" 120 | s = [] 121 | prev_ch_was_lower = False 122 | 123 | for i, ch in enumerate(self): 124 | if ch.isupper(): 125 | if i and prev_ch_was_lower: 126 | s.append("_") 127 | 128 | prev_ch_was_lower = False 129 | 130 | else: 131 | prev_ch_was_lower = True 132 | 133 | s.append(ch) 134 | return re.sub(r"[\s-]+", "_", "".join(s)).lower() 135 | 136 | def __format__(self, format_str): 137 | return String(self) 138 | 139 | 140 | class Stream(object): 141 | """A Stream object that pout needs to be able to write to something, an 142 | instance of some stream instance needs to be set in pout.stream. 143 | 144 | The only interface this object needs is the writeline() function, I thought 145 | about using an io ABC but that seemed more complicated than it was worth 146 | 147 | https://docs.python.org/3/library/io.html#class-hierarchy 148 | """ 149 | def writeline(self, s): 150 | """write out a line and add a newline at the end 151 | 152 | the requirement for the newline is because the children use the logging 153 | module and it prints a newline at the end by default in 2.7 (you can 154 | override it in >3.2) 155 | 156 | :param s: string, this will be written to the stream and a newline will 157 | be added to the end 158 | """ 159 | raise NotImplementedError() 160 | 161 | 162 | class StderrStream(Stream): 163 | """A stream object that writes out to stderr""" 164 | def __init__(self): 165 | # this is the pout printing logger, if it hasn't been touched it will be 166 | # configured to print to stderr, this is what is used in 167 | # pout_class._print() 168 | logger = logging.getLogger("stderr.{}".format(__name__.split(".")[0])) 169 | if len(logger.handlers) == 0: 170 | logger.setLevel(logging.DEBUG) 171 | log_handler = logging.StreamHandler(stream=sys.stderr) 172 | log_handler.setFormatter(logging.Formatter('%(message)s')) 173 | logger.addHandler(log_handler) 174 | logger.propagate = False 175 | 176 | self.logger = logger 177 | 178 | def writeline(self, s): 179 | self.logger.debug(String(s)) 180 | 181 | 182 | class FileStream(StderrStream): 183 | """A stream object that writes to a file path passed into it""" 184 | def __init__(self, path): 185 | logger = logging.getLogger("file.{}".format(__name__.split(".")[0])) 186 | if len(logger.handlers) == 0: 187 | logger.setLevel(logging.DEBUG) 188 | log_handler = logging.FileHandler(path) 189 | log_handler.setFormatter(logging.Formatter('%(message)s')) 190 | logger.addHandler(log_handler) 191 | logger.propagate = False 192 | 193 | self.logger = logger 194 | 195 | 196 | class OrderedItems(object): 197 | """Returns the items of the wrapped dict in alphabetical/sort order of the 198 | keys""" 199 | def __init__(self, d: dict): 200 | self.d = d 201 | 202 | def __iter__(self): 203 | keys = list(self.d.keys()) 204 | keys.sort() 205 | for k in keys: 206 | yield k, self.d[k] 207 | 208 | 209 | class Color(object): 210 | @classmethod 211 | def color_header(cls, text): 212 | """The color/formatting for section headers for things like classes""" 213 | return cls.color(text, bold=True) 214 | 215 | @classmethod 216 | def color_meta(cls, text): 217 | """The color/formatting for meta information""" 218 | return cls.color(text, "LIGHTGRAY") 219 | 220 | @classmethod 221 | def color_indent(cls, text): 222 | """The color/formatting for the indentation for each line of output""" 223 | return cls.color_meta(text) 224 | 225 | @classmethod 226 | def color_string(cls, text): 227 | """The color/formatting for string values""" 228 | return "".join( 229 | cls.color(line, "RED") for line in text.splitlines(True) 230 | ) 231 | #return cls.color(text, "RED") 232 | 233 | @classmethod 234 | def color_key(cls, text): 235 | """The color/formatting for dictionary keys and list indexes""" 236 | return cls.color(text, "CYAN") 237 | 238 | @classmethod 239 | def color_attr(cls, text): 240 | """The color/formatting for object attributes""" 241 | return cls.color(text, "BLUE") 242 | 243 | @classmethod 244 | def color_number(cls, text): 245 | """The color/formatting for numerical values""" 246 | return cls.color(text, "MAGENTA") 247 | 248 | @classmethod 249 | def color(cls, text, fg="", bg="", **kwargs): 250 | """Wrap text in fg and bg color 251 | 252 | See: 253 | - https://github.com/Jaymon/pout/issues/94 254 | - https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 255 | - https://unix.stackexchange.com/questions/105568/ 256 | 257 | Based off of this: 258 | https://github.com/django/django/blob/main/django/utils/termcolors.py 259 | 260 | The supported color names: 261 | - BLACK 262 | - RED 263 | - GREEN 264 | - YELLOW 265 | - BLUE 266 | - MAGENTA 267 | - CYAN 268 | - WHITE 269 | 270 | :param text: str, the text to wrap with fg and bg colors 271 | :param fg: str, the foreground color 272 | :param bg: str, the background color 273 | :keyword bold: bool, True to make text bold 274 | :keyword underline: bool, True to underline text 275 | :returns: str, text wrapped with terminal color if supported 276 | """ 277 | if text and environ.has_color_support(): 278 | color_names = ( 279 | "BLACK", 280 | "RED", 281 | "GREEN", 282 | "YELLOW", 283 | "BLUE", 284 | "MAGENTA", 285 | "CYAN", 286 | "WHITE" 287 | ) 288 | foreground = {color_names[x]: f"3{x}" for x in range(8)} 289 | background = {color_names[x]: f"4{x}" for x in range(8)} 290 | 291 | # bright versions 292 | for x in range(8): 293 | foreground[f"BRIGHT{color_names[x]}"] = f"9{x}" 294 | background[f"BRIGHT{color_names[x]}"] = f"10{x}" 295 | 296 | foreground["NONE"] = 0 297 | 298 | aliases = [ 299 | ("LIGHTGRAY", "WHITE"), 300 | ("BRIGHTGRAY", "BRIGHTBLACK"), 301 | ("GRAY", "WHITE"), 302 | ("PURPLE", "MAGENTA"), 303 | ] 304 | for k1, k2 in aliases: 305 | foreground[k1] = foreground[k2] 306 | background[k1] = background[k2] 307 | 308 | options = { 309 | "bold": "1", 310 | "underscore": "4", 311 | "underline": "4", 312 | "blink": "5", 313 | "reverse": "7", 314 | "conceal": "8", 315 | } 316 | 317 | codes = [] 318 | if fg: 319 | codes.append(foreground[fg.upper()]) 320 | 321 | if bg: 322 | codes.append(background[bg.upper()]) 323 | 324 | for k, v in kwargs.items(): 325 | if v: 326 | k = k.lower() 327 | if options.get(k, False): 328 | codes.append(options[k]) 329 | 330 | color = reset = "" 331 | if codes: 332 | color = "\033[{}m".format(";".join(codes)) 333 | reset = "\033[{}m".format(foreground["NONE"]) 334 | 335 | text = f"{color}{text}{reset}" 336 | 337 | return text 338 | 339 | -------------------------------------------------------------------------------- /pout/reflect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | import os 4 | import codecs 5 | import ast 6 | import re 7 | import logging 8 | import io 9 | import tokenize 10 | import functools 11 | 12 | from .compat import * 13 | from . import environ 14 | from .path import Path 15 | from .utils import String 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class CallString(String): 22 | """Contains the actual pout.* call, this is needed to find the argument 23 | names and stuff""" 24 | @property 25 | def tokens(self): 26 | # https://github.com/python/cpython/blob/3.7/Lib/token.py 27 | logger.debug("Callstring [{}] being tokenized".format(self)) 28 | 29 | tokenizer = getattr( 30 | tokenize, 31 | "_generate_tokens_from_c_tokenizer", 32 | None, 33 | ) 34 | 35 | if tokenizer: 36 | # we want to fail on extra tokens (I think) 37 | tokenizer = functools.partial(tokenizer, extra_tokens=False) 38 | 39 | else: 40 | # python 3.10 era will fail on extra tokens automatically 41 | tokenizer = tokenize.generate_tokens 42 | 43 | parens = [] 44 | return tokenizer(StringIO(self).readline) 45 | 46 | def is_complete(self): 47 | """Return True if this call string is complete, meaning it has a 48 | function name and balanced parens""" 49 | try: 50 | [t for t in self.tokens] 51 | ret = True 52 | logger.debug("CallString [{}] is complete".format(self.strip())) 53 | 54 | except tokenize.TokenError: 55 | logger.debug( 56 | "CallString [{}] is NOT complete".format(self.strip()) 57 | ) 58 | ret = False 59 | 60 | return ret 61 | 62 | def call_statements(self): 63 | statements = [] 64 | splitters = set([";", ":"]) 65 | 66 | statement = "" 67 | for token in self.tokens: 68 | if token.string in splitters: 69 | statements.append( 70 | type(self)(statement.strip()) 71 | ) 72 | 73 | statement = "" 74 | 75 | else: 76 | statement += token.string 77 | 78 | return statements 79 | 80 | def _append_name(self, arg_names, arg_name): 81 | n = "" 82 | is_string = [] 83 | in_root = True 84 | last_tok_end = -1 85 | for token in arg_name: 86 | # https://github.com/python/cpython/blob/3.7/Lib/token.py 87 | # https://github.com/python/cpython/blob/3.7/Lib/tokenize.py 88 | c = token.string 89 | if last_tok_end < 0: 90 | last_tok_end = token.end[1] 91 | else: 92 | n += " " * (token.start[1] - last_tok_end) 93 | last_tok_end = token.end[1] 94 | 95 | if token.type == tokenize.STRING and in_root: 96 | is_string.append(True) 97 | 98 | elif token.type == tokenize.NAME: 99 | if c == "in": 100 | is_string.append(False) 101 | 102 | else: 103 | if c in set(["[", "("]): 104 | in_root = False 105 | elif c in set(["]", ")"]): 106 | in_root = True 107 | 108 | n += c 109 | 110 | if is_string and all(is_string): 111 | arg_names.append("") 112 | logger.debug('Appending "{}" as a string'.format(n)) 113 | else: 114 | n = n.strip() 115 | if n: 116 | logger.debug('Appending "{}"'.format(n)) 117 | arg_names.append(n) 118 | 119 | def arg_names(self): 120 | arg_names = [] 121 | try: 122 | tokens = list(self.tokens) 123 | 124 | except tokenize.TokenError: 125 | return arg_names 126 | 127 | # let's find the ( 128 | token = tokens.pop(0) 129 | while token.string != "(": 130 | token = tokens.pop(0) 131 | 132 | 133 | # now we will divide by comma and find all the argument names 134 | arg_name = [] 135 | token = tokens.pop(0) 136 | #stop_c = set([")", ","]) 137 | stop_stack = [set([")", ","])] 138 | in_root = True 139 | while tokens and token.string != ";": 140 | c = token.string 141 | stop_c = stop_stack[-1] 142 | append_c = True 143 | logger.debug( 144 | 'Checking "{}" ({}), in_root={}'.format( 145 | c, 146 | tokenize.tok_name[token.type], 147 | in_root 148 | ) 149 | ) 150 | 151 | if c in stop_c: 152 | if in_root: 153 | #arg_names.append(self._normalize_name(arg_name)) 154 | self._append_name(arg_names, arg_name) 155 | #append_name(arg_name) 156 | arg_name = [] 157 | append_c = False 158 | 159 | else: 160 | stop_stack.pop() 161 | in_root = len(stop_stack) == 1 162 | #in_root = True 163 | #stop_c = set([")", ","]) 164 | 165 | else: 166 | if c == "(": 167 | in_root = False 168 | #stop_c = set([")"]) 169 | stop_stack.append(set([")"])) 170 | 171 | elif c == "[": 172 | in_root = False 173 | #stop_c = set(["]"]) 174 | stop_stack.append(set(["]"])) 175 | 176 | if append_c: 177 | arg_name.append(token) 178 | #pout2.v(arg_name) 179 | #print(arg_name) 180 | 181 | token = tokens.pop(0) 182 | 183 | #self._append_name(arg_names, arg_name) 184 | 185 | #pout2.v(arg_names) 186 | return arg_names 187 | 188 | 189 | class Call(object): 190 | """Wraps a generic frame_tuple returned from like inspect.stack() and makes 191 | the information containded in that FrameInfo tuple a little easier to 192 | digest 193 | 194 | since -- 7-2-12 -- Jay 195 | 196 | This wraps a .info dict that contains a bunch of information about the 197 | call: 198 | * line: int, what line the call originated on 199 | * file, str, the full filepath the call was made from 200 | * call, CallString|str, the full text of the call (currently, this 201 | might be missing a closing paren) 202 | * arg_names, list, the values passed to cthe call statement 203 | 204 | https://docs.python.org/3/library/inspect.html 205 | """ 206 | @classmethod 207 | def get_src_lines(cls, path): 208 | """Read the src file at path and return the lines as a list 209 | 210 | :param path: str|Path, the full path to the source file 211 | :returns: list[str], the lines of the source file or empty list if it 212 | couldn't be loaded 213 | """ 214 | try: 215 | open_kwargs = dict( 216 | mode='r', 217 | errors='replace', 218 | encoding=environ.ENCODING 219 | ) 220 | with open(path, **open_kwargs) as fp: 221 | return fp.readlines() 222 | 223 | except (IOError, SyntaxError) as e: 224 | # we failed to open the file, IPython has this problem 225 | return [] 226 | 227 | @classmethod 228 | def find_names(cls, called_module, called_func, ast_tree=None): 229 | """ 230 | scan the abstract source tree looking for possible ways to call the 231 | called_module and called_func 232 | 233 | since -- 7-2-12 -- Jay 234 | 235 | :example: 236 | # import the module a couple ways: 237 | import pout 238 | from pout import v 239 | from pout import v as voom 240 | import pout as poom 241 | 242 | # this function would return: ['pout.v', 'v', 'voom', 'poom.v'] 243 | 244 | module finder might be useful someday 245 | link -- http://docs.python.org/library/modulefinder.html 246 | link -- http://stackoverflow.com/questions/2572582/return-a-list-of-imported-python-modules-used-in-a-script 247 | 248 | :param ast_tree: _ast.* instance, the internal ast object that is being 249 | checked, returned from compile() with ast.PyCF_ONLY_AST flag 250 | :param called_module: str, we are checking the ast for imports of this 251 | module 252 | :param called_func: str, we are checking the ast for aliases of this 253 | function 254 | :returns: set, the list of possible calls the ast_tree could make to 255 | call the called_func 256 | """ 257 | s = set() 258 | 259 | func_name = called_func 260 | if not isinstance(called_func, str): 261 | func_name = called_func.__name__ 262 | 263 | module_name = called_module 264 | if not isinstance(called_module, str): 265 | module_name = called_module.__name__ 266 | 267 | # always add the default call, the set will make sure there are no 268 | # dupes... 269 | s.add("{}.{}".format(module_name, func_name)) 270 | 271 | if ast_tree: 272 | if hasattr(ast_tree, 'name'): 273 | if ast_tree.name == func_name: 274 | # the function is defined in this module 275 | s.add(func_name) 276 | 277 | if hasattr(ast_tree, 'body'): 278 | # further down the rabbit hole we go 279 | if isinstance(ast_tree.body, Iterable): 280 | for ast_body in ast_tree.body: 281 | s.update( 282 | cls.find_names( 283 | module_name, 284 | func_name, 285 | ast_body 286 | ) 287 | ) 288 | 289 | elif hasattr(ast_tree, 'names'): 290 | # base case 291 | if hasattr(ast_tree, 'module'): 292 | # we are in a from ... import ... statement 293 | if ast_tree.module == module_name: 294 | for ast_name in ast_tree.names: 295 | if ast_name.name == func_name: 296 | if ast_name.asname is None: 297 | s.add(ast_name.name) 298 | 299 | else: 300 | s.add(str(ast_name.asname)) 301 | 302 | else: 303 | # we are in an import ... statement 304 | for ast_name in ast_tree.names: 305 | if ( 306 | hasattr(ast_name, 'name') 307 | and (ast_name.name == module_name) 308 | ): 309 | if ast_name.asname is None: 310 | name = ast_name.name 311 | 312 | else: 313 | name = ast_name.asname 314 | 315 | call = "{}.{}".format( 316 | name, 317 | func_name 318 | ) 319 | s.add(call) 320 | 321 | return s 322 | 323 | @classmethod 324 | def find_call_info(cls, called_module, called_func, called_frame_info): 325 | """This has the same signature as .__init__ and is just here to 326 | get the caller frame info and then call .find_callstring_info 327 | 328 | :returns dict: see .find_callstring_info 329 | """ 330 | try: 331 | frames = inspect.getouterframes(called_frame_info.frame) 332 | caller_frame_info = frames[1] 333 | 334 | except Exception as e: 335 | #logger.exception(e) 336 | # the call was from the outermost script/module 337 | caller_frame_info = called_frame_info 338 | 339 | finally: 340 | call_info = cls.find_callstring_info( 341 | called_module, 342 | called_func, 343 | caller_frame_info 344 | ) 345 | 346 | return call_info 347 | 348 | @classmethod 349 | def find_callstring_info(cls, called_module, called_func, caller_frame_info): 350 | """Do the best we can to find the actual call string (ie, the function 351 | name and the arguments passed to the function when called) in the 352 | actual code 353 | 354 | This is where all the magic happens 355 | 356 | :param called_module: str|types.ModuleType, the module that was called, 357 | this should almost always be pout 358 | :param called_func: str|callable, this is the pout function that was 359 | called 360 | :param caller_frame_info: inspect.FrameInfo, this is the frame 361 | information about the caller (the code that called the module 362 | and func 363 | 364 | https://docs.python.org/3/library/inspect.html#the-interpreter-stack 365 | https://docs.python.org/3/reference/datamodel.html#frame-objects 366 | 367 | :returns: dict, a dictionary containing all the found information 368 | about the call 369 | """ 370 | call_info = {} 371 | 372 | call_info["call"] = "" 373 | call_info["call_modname"] = called_module 374 | call_info["call_funcname"] = called_func 375 | call_info["arg_names"] = [] 376 | 377 | call_info["file"] = Path(caller_frame_info.filename) 378 | call_info["line"] = caller_frame_info.lineno 379 | call_info["start_line"] = caller_frame_info.lineno 380 | call_info["stop_line"] = caller_frame_info.lineno 381 | 382 | if caller_frame_info.code_context is not None: 383 | src_lines = [] 384 | 385 | cs = CallString( 386 | caller_frame_info.code_context[caller_frame_info.index] 387 | ) 388 | if not cs.is_complete(): 389 | # our call statement is actually multi-line so we will need to 390 | # load the file to find the full statement 391 | if src_lines := cls.get_src_lines(call_info["file"]): 392 | total_lines = len(src_lines) 393 | start_lineno = call_info["line"] - 1 394 | stop_lineno = call_info["line"] + 1 395 | while not cs.is_complete() and stop_lineno <= total_lines: 396 | cs = CallString( 397 | "".join( 398 | src_lines[start_lineno:stop_lineno] 399 | ) 400 | ) 401 | stop_lineno += 1 402 | 403 | call_info["stop_line"] = stop_lineno - 1 404 | 405 | call_info["call"] = cs 406 | 407 | statements = cs.call_statements() 408 | if len(statements) > 1: 409 | # the line includes semi-colons so we need to find the correct 410 | # calling statement 411 | def get_call(statements, names): 412 | for statement in statements: 413 | for name in names: 414 | if statement.startswith(name): 415 | return statement 416 | 417 | names = cls.find_names(called_module, called_func) 418 | cs = get_call(statements, names) 419 | 420 | if not cs: 421 | if not src_lines: 422 | src_lines = cls.get_src_lines(call_info["file"]) 423 | 424 | if src_lines: 425 | # we failed to easily find the correct calling statement 426 | # so we are going to try a little harder this time 427 | ast_tree = compile( 428 | "".join(src_lines), 429 | call_info['file'], 430 | 'exec', 431 | ast.PyCF_ONLY_AST 432 | ) 433 | 434 | names = cls.find_names( 435 | called_module, 436 | called_func, 437 | ast_tree, 438 | ) 439 | cs = get_call(statements, names) 440 | 441 | if cs: 442 | call_info["arg_names"] = cs.arg_names() 443 | 444 | return call_info 445 | 446 | def __init__(self, called_module, called_func, called_frame_info): 447 | """Get information about the call 448 | 449 | :param called_module: str|types.ModuleType, the called module (should 450 | almost always be "pout" 451 | :param called_func: str|callable, the pout function that was called 452 | :param called_outer_frame: inspect.FrameInfo, the frame information 453 | for the actual call, this will be used to find the caller, one row 454 | of the inspect.getouterframes return list 455 | """ 456 | self.info = self.find_call_info( 457 | called_module, 458 | called_func, 459 | called_frame_info 460 | ) 461 | 462 | 463 | class Reflect(object): 464 | """This provides the meta information (file, line number) for the actual 465 | pout call 466 | """ 467 | def __init__(self, module, module_function_name, function_arg_vals): 468 | self.module = module 469 | self.module_function_name = module_function_name 470 | self.arg_vals = function_arg_vals or [] 471 | 472 | def __enter__(self): 473 | frame = frames = None 474 | 475 | try: 476 | # we want to get the frame of the current pout.* call 477 | frames = inspect.stack() 478 | frame = frames[1] 479 | self.call = Call( 480 | self.module.__name__, 481 | self.module_function_name, 482 | frame 483 | ) 484 | 485 | except IndexError as e: 486 | # There was a very specific bug that would cause 487 | # inspect.getouterframes(frame) to fail when pout was called from 488 | # an object's method that was called from within a Jinja template, 489 | # it seemed like it was going to be annoying to reproduce and so I 490 | # now catch the IndexError that inspect was throwing 491 | #logger.exception(e) 492 | self.call = None 493 | 494 | self.info = self._get_arg_info() 495 | 496 | return self 497 | 498 | def __exit__(self, exception_type, exception_val, trace): 499 | del self.call 500 | 501 | def _get_arg_info(self): 502 | ''' 503 | get all the info of a method call 504 | 505 | this will find what arg names you passed into the method and tie them 506 | to their passed in values, it will also find file and line number 507 | 508 | :returns: dict, a bunch of info on the call 509 | ''' 510 | ret_dict = { 511 | 'args': [], 512 | #'frame': None, 513 | 'line': 'Unknown', 514 | 'file': 'Unknown', 515 | 'arg_names': [] 516 | } 517 | #modname = self.modname 518 | 519 | c = self.call 520 | if c: 521 | ret_dict.update(c.info) 522 | 523 | arg_vals = self.arg_vals 524 | if len(arg_vals) > 0: 525 | args = [] 526 | 527 | if len(ret_dict['arg_names']) > 0: 528 | # match the found arg names to their respective values 529 | for i, arg_name in enumerate(ret_dict['arg_names']): 530 | try: 531 | args.append({'name': arg_name, 'val': arg_vals[i]}) 532 | 533 | except IndexError: 534 | # arg_vals[i] will fail with keywords passed into the 535 | # method 536 | break 537 | 538 | else: 539 | # we can't autodiscover the names, in an interactive shell 540 | # session? 541 | for i, arg_val in enumerate(arg_vals): 542 | args.append( 543 | {'name': 'Unknown {}'.format(i), 'val': arg_val} 544 | ) 545 | 546 | ret_dict['args'] = args 547 | 548 | return ret_dict 549 | 550 | -------------------------------------------------------------------------------- /tests/value_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import hmac 3 | import hashlib 4 | import array 5 | import re 6 | from pathlib import Path 7 | import datetime 8 | import uuid 9 | from collections import namedtuple 10 | 11 | from . import testdata, TestCase 12 | 13 | import pout 14 | from pout import environ 15 | from pout.compat import * 16 | from pout.value import ( 17 | PrimitiveValue, 18 | DictValue, 19 | DictProxyValue, 20 | ListValue, 21 | SetValue, 22 | TupleValue, 23 | NamedTupleValue, 24 | StringValue, 25 | BytesValue, 26 | InstanceValue, 27 | ExceptionValue, 28 | ModuleValue, 29 | TypeValue, 30 | RegexValue, 31 | RegexMatchValue, 32 | GeneratorValue, 33 | CallableValue, 34 | Value, 35 | ) 36 | 37 | 38 | class ValueTest(TestCase): 39 | def test_primitive_int(self): 40 | v = Value(100) 41 | r = v.string_value() 42 | print(r) 43 | return 44 | 45 | v = Value(100, show_instance_type=True) 46 | r = v.string_value() 47 | self.assertFalse("Instance Properties" in r, r) 48 | self.assertTrue("int instance" in r) 49 | self.assertTrue("100" in r) 50 | self.assertTrue("<" in r) 51 | 52 | def test_primitive_bool(self): 53 | v = Value(True, show_instance_type=True) 54 | r = v.string_value() 55 | self.assertFalse("Instance Properties" in r, r) 56 | self.assertTrue("bool instance" in r) 57 | self.assertTrue("True" in r) 58 | self.assertTrue("<" in r) 59 | 60 | def test_primitive_float(self): 61 | v = Value(123456.789, show_instance_type=True) 62 | r = v.string_value() 63 | self.assertFalse("Instance Properties" in r, r) 64 | self.assertTrue("float instance" in r) 65 | self.assertTrue("123456.789" in r) 66 | self.assertTrue("<" in r) 67 | 68 | def test_primitive_none(self): 69 | v = Value(None, show_instance_type=True) 70 | r = v.string_value() 71 | self.assertFalse("Instance Properties" in r, r) 72 | self.assertTrue("NoneType instance" in r) 73 | self.assertTrue("<" in r) 74 | 75 | def test_iterate_object_depth(self): 76 | """dicts, lists, etc. should also be subject to OBJECT_DEPTH limits""" 77 | t = { 78 | "foo": 1, 79 | "bar": { 80 | "che": 2, 81 | "boo": { 82 | "baz": 3, 83 | "moo": { 84 | "maz": 4, 85 | } 86 | } 87 | } 88 | } 89 | v = Value(t, OBJECT_DEPTH=1) 90 | c = v.string_value() 91 | self.assertTrue("'bar': .Foo" in r) 158 | 159 | def bar(one, two): 160 | pass 161 | 162 | r = Value(bar)._get_name(bar) 163 | self.assertTrue(".bar" in r) 164 | 165 | def test_descriptor(self): 166 | class Foo(object): 167 | @property 168 | def bar(self): 169 | return 1 170 | 171 | f = Foo() 172 | v = Value(f) 173 | s = v.string_value() 174 | self.assertTrue("` entries, this makes 492 | sure that is fixed 493 | 494 | .. example: 495 | l = [1, 1, 1, 1] 496 | pout.v(l) 497 | l = list (4) 498 | ܁ [ 499 | ܁ ܁ 0: 1, 500 | ܁ ܁ 1: , 501 | ܁ ܁ 2: , 502 | ܁ ܁ 3: 503 | ܁ ] 504 | """ 505 | l = [1, 1] 506 | v = Value(l) 507 | s = v.string_value() 508 | self.assertEqual(3, s.count("1")) 509 | 510 | l = ["one", "one"] 511 | v = Value(l) 512 | s = v.string_value() 513 | self.assertEqual(2, s.count("one")) 514 | 515 | l = [True, True] 516 | v = Value(l) 517 | s = v.string_value() 518 | self.assertEqual(2, s.count("True")) 519 | 520 | l = [None, None] 521 | v = Value(l) 522 | s = v.string_value() 523 | self.assertEqual(2, s.count("None")) 524 | 525 | def test_object_1(self): 526 | class FooObject(object): 527 | bar = 1 528 | che = "2" 529 | 530 | o = FooObject() 531 | o.baz = [3] 532 | indent = environ.INDENT_STRING 533 | 534 | v = Value(o) 535 | self.assertTrue(isinstance(v, InstanceValue)) 536 | 537 | r = v.string_value() 538 | self.assertRegex(r, rf"\n[{indent}]+<\n") 539 | 540 | d = { 541 | "che": o, 542 | "baz": {"foo": 1, "bar": 2} 543 | } 544 | v = Value(d) 545 | r = v.string_value() 546 | self.assertTrue(f"\n{indent * 3}<\n" in r) 547 | 548 | def test_object_2(self): 549 | class To26(object): 550 | value = 1 551 | class To25(object): 552 | instances = [To26()] 553 | class To24(object): 554 | instances = [To25()] 555 | class To23(object): 556 | instances = [To24()] 557 | class To22(object): 558 | instances = [To23()] 559 | class To21(object): 560 | instances = [To22()] 561 | instance = To22() 562 | 563 | t = To21() 564 | c1 = Value(t, OBJECT_DEPTH=10).string_value() 565 | c2 = Value(t, OBJECT_DEPTH=1).string_value() 566 | self.assertNotEqual(c1, c2) 567 | 568 | def test_object_3(self): 569 | """in python2 there was an issue with printing lists with unicode, this 570 | was traced to using Value.__repr__ which was returning a byte string in 571 | python2 which was then being cast to unicode and failing the conversion 572 | to ascii""" 573 | class To3(object): 574 | pass 575 | 576 | t = To3() 577 | t.foo = [ 578 | testdata.get_unicode_words(), 579 | testdata.get_unicode_words(), 580 | ] 581 | 582 | # no UnicodeError raised is success 583 | Value(t).string_value() 584 | 585 | def test_object_4_recursive(self): 586 | class To4(object): 587 | def __str__(self): 588 | return self.render() 589 | 590 | def render(self): 591 | pout.v(self) 592 | return self.__class__.__name__ 593 | 594 | t = To4() 595 | pout.v(t) 596 | 597 | def test_object___pout___1(self): 598 | class OPU(object): 599 | def __pout__(self): 600 | return "foo" 601 | 602 | v = Value(OPU()) 603 | s = v.string_value() 604 | self.assertEqual(4, len(s.splitlines(False))) 605 | self.assertTrue("foo" in s) 606 | 607 | def test_object___pout___unicode(self): 608 | s = testdata.get_unicode_words() 609 | class OPU(object): 610 | def __pout__(self): 611 | return {"foo": s} 612 | 613 | o = OPU() 614 | c = Value(o).string_value() 615 | self.assertTrue(s in c) 616 | 617 | def test_object___pout___class(self): 618 | """The __pout__ method could cause failure when defined on a class 619 | and the class is being outputted because __pout__ is an instance 620 | method, this makes sure __pout__ failing doesn't fail the whole thing 621 | """ 622 | class Foo(object): 623 | def __pout__(self): 624 | return 1 625 | 626 | v = Value(Foo) 627 | s = v.string_value() 628 | self.assertTrue(".Foo" in s) 629 | 630 | def test_std_collections__pout__(self): 631 | """https://github.com/Jaymon/pout/issues/61""" 632 | class PoutDict(dict): 633 | def __pout__(self): 634 | return "custom dict" 635 | 636 | d = PoutDict(foo=1, bar=2) 637 | v = Value(d) 638 | s = v.string_value() 639 | self.assertTrue("custom dict" in s) 640 | 641 | def test_object_string_limit(self): 642 | class StrLimit(object): 643 | def __str__(self): 644 | return testdata.get_words(100) 645 | 646 | s = StrLimit() 647 | v = Value(s) 648 | r = v.string_value() 649 | self.assertTrue("... Truncated " in r) 650 | 651 | def test_type_1(self): 652 | class Foo(object): 653 | bar = 1 654 | 655 | v = Value(Foo) 656 | s = v.string_value() 657 | #self.assertTrue("bar = int instance" in s, s) 658 | self.assertRegex(s, r"bar\s=\s1\s", s) 659 | 660 | v = Value(object, show_instance_id=True, show_instance_type=True) 661 | self.assertTrue(isinstance(v, TypeValue)) 662 | 663 | s = v.string_value() 664 | self.assertRegex(s, r"object\sclass\sat\s\dx[^>]+?", s) 665 | 666 | def test_regex_match(self): 667 | m = re.match(r"(\d)(\d)(\d+)", "0213434") 668 | v = Value(m) 669 | self.assertTrue(isinstance(v, RegexMatchValue)) 670 | 671 | r = v.string_value() 672 | self.assertTrue("Pattern:" in r) 673 | self.assertTrue("Group 3" in r) 674 | 675 | def test_regex_compiled(self): 676 | regex = re.compile(r"^\s([a-z])", flags=re.I) 677 | v = Value(regex) 678 | self.assertTrue(isinstance(v, RegexValue)) 679 | 680 | r = v.string_value() 681 | self.assertTrue("re:Pattern" in r) 682 | self.assertTrue("groups:" in r) 683 | self.assertTrue("flags:" in r) 684 | 685 | def test_callable_1(self): 686 | class Klass(object): 687 | def instancemethod(self, *args, **kwargs): pass 688 | @classmethod 689 | def clsmethod(cls, *args, **kwargs): pass 690 | 691 | v = Value(Klass.clsmethod) 692 | s = v.string_value() 693 | self.assertTrue("" in s) 873 | self.assertTrue("foo" in s) 874 | 875 | def test_uuid(self): 876 | v = Value(uuid.uuid4()) 877 | s = v.string_value() 878 | self.assertTrue("UUID" in s) 879 | self.assertTrue("<" in s) 880 | 881 | v = Value(uuid.uuid4(), show_simple_value=False) 882 | s = v.string_value() 883 | self.assertTrue("UUID" in s) 884 | self.assertTrue("version:" in s) 885 | self.assertTrue("<" in s) 886 | 887 | v = Value(uuid.uuid4(), show_simple=True) 888 | s = v.string_value() 889 | self.assertTrue(s.startswith("\"")) 890 | self.assertTrue(s.endswith("\"")) 891 | self.assertFalse("\n" in s) 892 | 893 | def test_trailing_whitespace(self): 894 | v = Value([1, 2]) 895 | s = v.string_value() 896 | self.assertFalse(s[-1].isspace()) 897 | 898 | def test_module_output(self): 899 | v = Value(testdata) 900 | s = v.string_value() 901 | for header in ["Properties", "Classes", "Functions", "Modules"]: 902 | self.assertTrue(header in s) 903 | 904 | def test_namedtuple(self): 905 | Foo = namedtuple("Foo", "bar che") 906 | 907 | f = Foo(1, 2) 908 | v = Value(f) 909 | s = v.string_value() 910 | self.assertTrue("namedtuple" in s) 911 | self.assertTrue("0 bar" in s) 912 | self.assertTrue("1 che" in s) 913 | 914 | -------------------------------------------------------------------------------- /tests/interface_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import time 4 | import hmac 5 | import hashlib 6 | import subprocess 7 | import os 8 | import re 9 | import logging 10 | import json 11 | 12 | # this is the local pout that is going to be tested 13 | import pout 14 | from pout.compat import * 15 | from pout.interface import V 16 | from pout import environ 17 | 18 | from . import testdata, TestCase 19 | 20 | 21 | class Foo(object): 22 | bax=4 23 | def __init__(self): 24 | self.bar = 1 25 | self.che = 2 26 | self.baz = 3 27 | 28 | def raise_error(self): 29 | e = IndexError("foo") 30 | raise e 31 | 32 | 33 | class Bar(object): 34 | 35 | f = Foo() 36 | 37 | def __init__(self): 38 | self.foo = 1 39 | self.che = 2 40 | self.baz = 3 41 | 42 | def __str__(self): 43 | return u"Bar" 44 | 45 | 46 | class FooBar(Foo, Bar): 47 | pass 48 | 49 | 50 | class Foo2(Foo): 51 | pass 52 | 53 | 54 | class Foo3(Foo2): 55 | pass 56 | 57 | 58 | class Che(object): 59 | 60 | f = Foo() 61 | b = Bar() 62 | 63 | def __getattr__(self, key): 64 | return super(Che, self).__getattr__(key) 65 | 66 | def __str__(self): 67 | return u"Che" 68 | 69 | 70 | class Bax(): 71 | ''' 72 | old school defined class that doesn't inherit from object 73 | ''' 74 | pass 75 | 76 | 77 | def baz(): 78 | pass 79 | 80 | 81 | class Bam(object): 82 | baz = "baz class property" 83 | che = "che class property" 84 | 85 | @property 86 | def bax(self): 87 | return "bax property" 88 | 89 | @classmethod 90 | def get_foo(cls): 91 | return "get_foo instance method" 92 | 93 | def __init__(self): 94 | self.baz = "baz instance property" 95 | 96 | def get_bar(self): 97 | return "get_bar instance method" 98 | 99 | 100 | class CTest(TestCase): 101 | def test_c(self): 102 | with testdata.capture() as c: 103 | pout.c('this is the input') 104 | self.assertTrue("Total Characters: 17" in c) 105 | 106 | with testdata.capture() as c: 107 | pout.c('\u304f') 108 | self.assertTrue("Total Characters: 1" in c) 109 | 110 | with testdata.capture() as c: 111 | pout.c('just\r\ntesting') 112 | self.assertTrue("Total Characters: 13" in c) 113 | 114 | with testdata.capture() as c: 115 | pout.c('just', u'testing') 116 | self.assertTrue("Total Characters: 4" in c) 117 | self.assertTrue("Total Characters: 7" in c) 118 | 119 | # !!! py2 thinks it is 2 chars 120 | with testdata.capture() as c: 121 | pout.c('\U00020731') 122 | self.assertTrue("Total Characters:" in c) 123 | 124 | def test_variables(self): 125 | v = "foo bar" 126 | with testdata.capture() as c: 127 | pout.c(v) 128 | #self.assertTrue("v (7) = " in c) 129 | self.assertRegex(str(c), r"^\s+v\s+=\s+") 130 | 131 | 132 | class JTest(TestCase): 133 | def test_j(self): 134 | with testdata.capture() as c: 135 | pout.j('["foo", {"bar":["baz", null, 1.0, 2]}]') 136 | self.assertTrue("list (2)" in c) 137 | 138 | with testdata.capture() as c: 139 | s = json.dumps({"foo": 1, "bar": 2}) 140 | pout.j(s) 141 | self.assertTrue("dict (2)" in c) 142 | 143 | 144 | class BTest(TestCase): 145 | def test_variable_1(self): 146 | s = "foo" 147 | with testdata.capture() as c: 148 | pout.b(s) 149 | self.assertTrue("foo" in c) 150 | 151 | s = b"foo" 152 | with testdata.capture() as c: 153 | pout.b(s) 154 | self.assertTrue("foo" in c) 155 | 156 | def test_variable_2(self): 157 | """https://github.com/Jaymon/pout/issues/40""" 158 | i = 5 159 | with testdata.capture() as c: 160 | pout.b(i) 161 | self.assertTrue(" 5 " in c) 162 | 163 | with testdata.capture() as c: 164 | pout.b(5) 165 | self.assertEqual(8, len(c.splitlines())) 166 | 167 | def test_b(self): 168 | with testdata.capture() as c: 169 | pout.b() 170 | self.assertTrue("*" in c) 171 | 172 | with testdata.capture() as c: 173 | pout.b(5) 174 | self.assertTrue("*" in c) 175 | 176 | with testdata.capture() as c: 177 | pout.b('this is the title') 178 | self.assertTrue("* this is the title *" in c) 179 | 180 | with testdata.capture() as c: 181 | pout.b('this is the title 2', 5) 182 | self.assertTrue("* this is the title 2 *" in c) 183 | 184 | with testdata.capture() as c: 185 | pout.b('this is the title 3', 3, '=') 186 | self.assertTrue("= this is the title 3 =" in c) 187 | 188 | 189 | class PTest(TestCase): 190 | def test_p_one_level(self): 191 | with testdata.capture() as c: 192 | pout.p('foo') 193 | time.sleep(.25) 194 | pout.p() 195 | self.assertTrue("foo - " in c) 196 | 197 | def test_p_multi_levels(self): 198 | with testdata.capture() as c: 199 | pout.p('multi foo') 200 | pout.p(u'multi bar') 201 | time.sleep(0.25) 202 | pout.p() 203 | time.sleep(0.25) 204 | pout.p() 205 | self.assertTrue("multi foo > multi bar" in c) 206 | self.assertTrue("multi foo -" in c) 207 | 208 | def test_p_with(self): 209 | with testdata.capture() as c: 210 | with pout.p("with foo"): 211 | time.sleep(0.25) 212 | self.assertTrue("with foo -" in c) 213 | 214 | 215 | class XTest(TestCase): 216 | def test_x(self): 217 | path = testdata.create_file([ 218 | "import pout", 219 | "", 220 | "v = 'xx'", 221 | "pout.x(v)" 222 | ]) 223 | #raise unittest.SkipTest("we skip the pout.x tests unless we are working on them") 224 | #pout.x() 225 | r = path.run(code=1) 226 | self.assertTrue("xx" in r) 227 | 228 | path = testdata.create_file([ 229 | "import pout", 230 | "", 231 | "pout.x()" 232 | ]) 233 | r = path.run(code=1) 234 | self.assertTrue("exit at line " in r) 235 | 236 | 237 | class SleepTest(TestCase): 238 | def test_run(self): 239 | with testdata.capture() as c: 240 | pout.sleep(0.25) 241 | 242 | self.assertTrue("Done Sleeping" in c) 243 | self.assertTrue("Sleeping 0.25 seconds" in c) 244 | 245 | def test_sleep(self): 246 | start = time.time() 247 | pout.sleep(1.1) 248 | stop = time.time() 249 | self.assertLess(1.0, stop - start) 250 | 251 | 252 | class TTest(TestCase): 253 | """test the pout.t() method""" 254 | def get_trace(self): 255 | pout.t() 256 | 257 | def test_t_1(self): 258 | with testdata.capture() as c: 259 | pout.t() 260 | self.assertTrue("pout.t()" in c) 261 | 262 | def test_t_2_space(self): 263 | with testdata.capture() as c: 264 | pout.t () 265 | self.assertTrue("pout.t ()" in c) 266 | 267 | def test_t_3_newline(self): 268 | with testdata.capture() as c: 269 | pout.t( 270 | inspect_packages=False, 271 | depth=0, 272 | ) 273 | indent = environ.INDENT_STRING 274 | self.assertRegex( 275 | str(c), 276 | ( 277 | r"pout.t\(\n" 278 | rf"[{indent}]+inspect_packages=False,\n" 279 | rf"[{indent}]+depth=0,\n" 280 | rf"[{indent}]+\)" 281 | ) 282 | ) 283 | 284 | def test_t_with_assign(self): 285 | ''' 286 | there was a problem where the functions to parse the call would fail 287 | when one of the inputs was a dict key assignment, this test makes sure that 288 | is fixed 289 | 290 | since -- 10-8-12 -- Jay 291 | ''' 292 | with testdata.capture() as c: 293 | r = {} 294 | r['foo'] = self.get_trace() 295 | self.assertTrue("get_trace()" in c) 296 | self.assertTrue("pout.t()" in c) 297 | 298 | 299 | class HTest(TestCase): 300 | """ 301 | test the pout.h() method 302 | """ 303 | def test_h(self): 304 | with testdata.capture() as c: 305 | pout.h(1) 306 | 307 | pout.h() 308 | self.assertTrue("here 1" in c) 309 | 310 | 311 | class STest(TestCase): 312 | def test_s_return(self): 313 | v = "foo" 314 | r = pout.s(v) 315 | self.assertTrue('v = ' in r) 316 | self.assertTrue('str (3)' in r) 317 | self.assertTrue('foo' in r) 318 | 319 | def test_ss_return(self): 320 | v = "foo" 321 | r = pout.ss(v) 322 | self.assertTrue('foo' in r) 323 | 324 | 325 | class RTest(TestCase): 326 | def test_run(self): 327 | path = testdata.create_file([ 328 | "# -*- coding: utf-8 -*-", 329 | "import pout", 330 | "", 331 | "for x in range(10):", 332 | " pout.r(x)", 333 | "", 334 | "for y in range(5):", 335 | " pout.r(y)", 336 | ]) 337 | c = path.run() 338 | self.assertTrue("pout.r(x) called 10 times" in c) 339 | self.assertTrue("pout.r(y) called 5 times" in c) 340 | 341 | 342 | class VTest(TestCase): 343 | def test_issue42(self): 344 | class Foo(object): 345 | def __init__(self, a, b): 346 | self.a = a 347 | self.b = b 348 | pout.v(self) 349 | 350 | f = Foo(1, 2) # success if no error raised 351 | 352 | def test_issue16(self): 353 | """ https://github.com/Jaymon/pout/issues/16 """ 354 | class Module(object): pass 355 | ret = "foo" 356 | default_val = "bar" 357 | self.issue_module = Module() 358 | self.issue_fields = {} 359 | k = "che" 360 | 361 | with testdata.capture() as c: 362 | pout.v( 363 | ret, 364 | default_val, 365 | getattr(self.issue_module, k, None), 366 | self.issue_fields.get(k, None) 367 | ) 368 | self.assertTrue('ret = ' in c) 369 | self.assertTrue('default_val =' in c) 370 | self.assertTrue('getattr(self.issue_module, k, None) = None' in c) 371 | self.assertTrue('self.issue_fields.get(k, None) = None') 372 | 373 | del self.issue_module 374 | del self.issue_fields 375 | 376 | def test_function(self): 377 | b = Bam() 378 | 379 | with testdata.capture() as c: 380 | pout.v(b.get_bar) 381 | self.assertTrue("method" in c) 382 | 383 | with testdata.capture() as c: 384 | pout.v(b.get_foo) 385 | self.assertTrue("method" in c) 386 | 387 | with testdata.capture() as c: 388 | pout.v(baz) 389 | self.assertTrue("function" in c) 390 | 391 | def test_get_name(self): 392 | """makes sure if __getattr__ raises other errors than AttributeError 393 | then pout will still print correctly""" 394 | class FooGetName(object): 395 | def __init__(self): 396 | self.fields = {} 397 | def __getattr__(self, key): 398 | # This will raise a KeyError when key doesn't exist 399 | return self.fields[key] 400 | 401 | with testdata.capture() as c: 402 | fgn = FooGetName() 403 | pout.v(fgn) 404 | 405 | for s in ["FooGetName (", "at 0x", "__str__ (", "fields = "]: 406 | self.assertTrue(s in c, s) 407 | 408 | def test_vs(self): 409 | with testdata.capture() as c: 410 | d = {'foo': 1, 'bar': 2} 411 | pout.vv(d) 412 | self.assertFalse("d (" in c) 413 | self.assertTrue("'foo':" in c) 414 | self.assertTrue("'bar':" in c) 415 | 416 | def test_issue_31(self): 417 | """https://github.com/Jaymon/pout/issues/31""" 418 | class Issue31String(String): 419 | def bar(self): 420 | pout.v("") 421 | return "" 422 | 423 | with testdata.capture() as c: 424 | s = Issue31String("foo") 425 | pout.v(s.bar()) 426 | 427 | lines = re.findall(r"\([^:)]+:\d+\)", str(c)) 428 | self.assertEqual(2, len(lines)) 429 | self.assertNotEqual(lines[0], lines[1]) 430 | 431 | def test_issue_34(self): 432 | """https://github.com/Jaymon/pout/issues/34""" 433 | class FooIssue34(object): 434 | bar_che = ["one", "two", "three"] 435 | 436 | left = "left" 437 | right = "right" 438 | 439 | with testdata.capture() as c: 440 | pout.v(left, " ".join(FooIssue34.bar_che), right) 441 | self.assertTrue("right" in c) 442 | self.assertTrue("left" in c) 443 | self.assertTrue("one two three" in c) 444 | 445 | def test_keys(self): 446 | d = {'\xef\xbb\xbffoo': ''} 447 | d = {'\xef\xbb\xbffo_timestamp': ''} 448 | pout.v(d) 449 | 450 | d = {0: "foo", 1: "bar"} 451 | pout.v(d) 452 | 453 | def test_compiled_regex(self): 454 | regex = re.compile(r"foo", re.I | re.MULTILINE) 455 | pout.v(regex) 456 | 457 | def test_cursor(self): 458 | import sqlite3 459 | path = ":memory:" 460 | con = sqlite3.connect(path) 461 | cur = con.cursor() 462 | pout.v(cur) 463 | 464 | def test_encoding_in_src_file(self): 465 | path = testdata.create_file([ 466 | "# -*- coding: iso-8859-1 -*-", 467 | "import pout", 468 | "", 469 | "# \u0087\u00EB", 470 | "# Här", 471 | "", 472 | "try:", 473 | " pout.v('foo bar')", 474 | "except Exception as e:", 475 | " print(e)", 476 | ]) 477 | 478 | # convert encoding to ISO-8859-1 from UTF-8, this is convoluted because 479 | # I usually never have to do this 480 | contents = path.read_text() 481 | path.write_text(contents, encoding="iso-8859-1") 482 | #path.encoding = "iso-8859-1" 483 | #path.replace(contents) 484 | 485 | environ = { 486 | "PYTHONPATH": os.path.abspath(os.path.expanduser(".")) 487 | } 488 | output = subprocess.check_output( 489 | [sys.executable, path], 490 | env=environ, 491 | stderr=subprocess.STDOUT, 492 | ) 493 | self.assertTrue("foo bar" in output.decode("utf-8")) 494 | 495 | def test_unicode_in_src_file(self): 496 | path = testdata.create_file([ 497 | "# -*- coding: utf-8 -*-", 498 | "import pout", 499 | "", 500 | "# {}".format(testdata.get_unicode_words()), 501 | "", 502 | "pout.v('foo bar')" 503 | ]) 504 | 505 | environ = { 506 | "PYTHONPATH": os.path.abspath(os.path.expanduser(".")) 507 | } 508 | output = subprocess.check_output( 509 | [sys.executable, path], 510 | env=environ, 511 | stderr=subprocess.STDOUT, 512 | ) 513 | self.assertTrue("foo bar" in output.decode("utf-8")) 514 | 515 | def test_depth(self): 516 | t = () 517 | for x in [8, 7, 6, 5, 4, 3, 2, 1]: 518 | t = (x, t) 519 | 520 | pout.v(t) 521 | 522 | def test_map(self): 523 | v = map(str, range(5)) 524 | pout.v(v) 525 | 526 | def test_binary_bytes(self): 527 | """https://github.com/Jaymon/pout/issues/30""" 528 | with testdata.capture() as c: 529 | s = b"foo" 530 | pout.v(bytes(s)) 531 | self.assertTrue("foo" in c) 532 | self.assertTrue("b\"" in c) 533 | 534 | def test_binary_1(self): 535 | with testdata.capture() as c: 536 | v = memoryview(b'abcefg') 537 | pout.v(v) 538 | 539 | v = bytearray.fromhex('2Ef0 F1f2 ') 540 | pout.v(v) 541 | 542 | if is_py2: 543 | v = bytes("foobar") 544 | else: 545 | v = bytes("foobar", "utf-8") 546 | pout.v(v) 547 | 548 | for s in ["b\"", "abcefg", "foobar", ".\\xf0\\xf1\\xf2"]: 549 | self.assertTrue(s in c, s) 550 | 551 | def test_binary_unicode_error(self): 552 | d = hmac.new(b"this is the key", b"this is the message", hashlib.md5) 553 | with testdata.capture() as c: 554 | pout.v(d.digest()) 555 | self.assertTrue("d.digest()" in c) 556 | self.assertTrue(" b\"" in c) 557 | self.assertFalse("" in c) 608 | self.assertTrue("m = " in c) 609 | 610 | def test_proxy_dict(self): 611 | with testdata.capture() as c: 612 | pout.v(FooBar.__dict__) 613 | for s in ["FooBar.__dict__", " (2)", "{"]: 614 | self.assertTrue(s in c, s) 615 | 616 | def test_multiline_comma(self): 617 | # https://github.com/Jaymon/pout/issues/12 618 | with testdata.capture() as c: 619 | pout.v( 620 | "foo", 621 | "bar", 622 | "che", 623 | ) 624 | self.assertTrue("foo" in c) 625 | self.assertTrue("bar" in c) 626 | self.assertTrue("che" in c) 627 | 628 | def test_type(self): 629 | with testdata.capture() as c: 630 | pout.v(type([])) 631 | self.assertTrue("type([]) =" in c) 632 | self.assertTrue(" will actually call 107 | this method, and this method will, in turn, call __call__ 108 | 109 | :param *args: mixed, the arguments passed to pout. 110 | :param **kwargs: mixed, the keyword arguments passed to 111 | pout. plus some other arguments that were bound to 112 | like `pout_module` and `pout_function_name` 113 | :returns: mixed, whatever __call__ returns 114 | """ 115 | module = kwargs["pout_module"] 116 | module_function_name = kwargs["pout_function_name"] 117 | instance_class = kwargs["pout_interface_class"] 118 | 119 | with Reflect(module, module_function_name, args) as r: 120 | instance = instance_class(r, module.stream) 121 | return instance(*args, **kwargs) 122 | 123 | def __init__(self, reflect, stream): 124 | self.reflect = reflect 125 | self.stream = stream 126 | 127 | def __init_subclass__(cls): 128 | """Called when a child class is loaded into memory 129 | 130 | https://peps.python.org/pep-0487/ 131 | """ 132 | cls.classes[cls.__name__.lower()] = cls 133 | 134 | def writeline(self, s): 135 | """Actually write s to something using self.stream""" 136 | self.stream.writeline(s) 137 | 138 | def writelines(self, ss): 139 | """Write a list of string to something using self.stream""" 140 | for s in ss: 141 | self.writeline(s) 142 | 143 | def _get_path(self, path): 144 | return self.path_class(path) 145 | 146 | def _printstr(self, args): 147 | """this gets all the args ready to be printed, this is terribly named 148 | """ 149 | s = "\n" 150 | 151 | for arg in args: 152 | #s += arg.encode('utf-8', 'pout.replace') 153 | s += arg 154 | 155 | return s 156 | 157 | def name_value(self, name, body, **kwargs): 158 | """normalize name. This will only do something with name if it has a 159 | value 160 | 161 | this method is called after .body_value(). This method respects 162 | SHOW_META and SHOW_NAME. This method will get called once for each 163 | tuple value [0] yielded from .input() 164 | 165 | :param name: str, the name value to use 166 | :param body: Any, the original un-normalized body, this is handy to 167 | have in case you want to tie the normalized name to the body in 168 | some way 169 | :param **kwargs: dict, anything passed into the interface 170 | :returns: str, the name normalized 171 | """ 172 | s = "" 173 | 174 | if name: 175 | show_meta = kwargs.get("show_meta", self.SHOW_META) 176 | show_name = show_meta and kwargs.get("show_name", self.SHOW_NAME) 177 | if show_name: 178 | s = name 179 | 180 | if s: 181 | s = Color.color_attr(s) 182 | 183 | return s 184 | 185 | def body_value(self, body, **kwargs): 186 | """normalize body 187 | 188 | this method is called from .output(). This method will get called once 189 | for each tuple value [1] yielded from .input() 190 | 191 | :param body: mixed, one of the inputted body 192 | :param **kwargs: dict, anything passed into the interface 193 | :returns: str, the body normalized 194 | """ 195 | return body 196 | 197 | def path_value(self, **kwargs): 198 | """normalize and return the path the interface was called from 199 | 200 | This method respects SHOW_META and SHOW_PATH 201 | 202 | :param **kwargs: dict, anything passed into the interface 203 | :returns: str, the path that should be included with the .name_value() 204 | and .body_value() values 205 | """ 206 | s = "" 207 | 208 | show_meta = kwargs.get("show_meta", self.SHOW_META) 209 | show_path = show_meta and kwargs.get("show_path", self.SHOW_PATH) 210 | if show_path: 211 | call_info = self.reflect.info 212 | if call_info: 213 | s = "({}:{})".format( 214 | self._get_path(call_info['file']), 215 | call_info['line'] 216 | ) 217 | 218 | if s: 219 | s = Color.color_meta(s) 220 | 221 | return s 222 | 223 | def input(self, *args, **kwargs): 224 | """normalize the arguments passed into the interface 225 | 226 | this is called from .output() and is used by .output() to put together 227 | the full output 228 | 229 | :param *args: list, the arguments passed into the interface 230 | :param **kwargs: dict, the keyword arguments passed into the interface 231 | :returns: generator of tuples, this will yield a tuple of (name, body), 232 | either of the values can be None. If no arguments were passed in 233 | then this will yield one time with (None, None) 234 | """ 235 | if args: 236 | for arg in args: 237 | yield None, arg 238 | 239 | else: 240 | # if we don't have any arguments we want .output() to do one 241 | # iteration 242 | yield None, None 243 | 244 | def output(self, *args, **kwargs): 245 | """Iterates through .input() and converts it in a format that can be 246 | printed 247 | 248 | This will iterate through .input() and call .body_value() and 249 | .name_value() for each tuple yielded. After all input has been yielded 250 | this will call .path_value() 251 | 252 | :returns: str, a string ready to be printed or returned 253 | """ 254 | bodies = [] 255 | for n, b in self.input(*args, **kwargs): 256 | body = self.body_value(b, **kwargs) 257 | name = self.name_value(n, b, **kwargs) 258 | 259 | if name: 260 | bodies.append("{} = {}".format(name, body)) 261 | 262 | else: 263 | bodies.append(body) 264 | 265 | path = self.path_value(**kwargs) 266 | if path: 267 | bodies.append(path) 268 | bodies.append("\n") 269 | 270 | return self._printstr(bodies) 271 | 272 | def __call__(self, *args, **kwargs): 273 | """Whenever a bound is invoked, this method is called 274 | 275 | This method respects PRINT_OUTPUT and RETURN_OUTPUT 276 | 277 | :param *args: mixed, the module. args 278 | :param **kwargs: mixed, the module. kwargs plus extra 279 | bound keywords 280 | :returns: mixed, whatever you want module. to return 281 | """ 282 | kwargs.setdefault("print_output", self.PRINT_OUTPUT) 283 | kwargs.setdefault("return_output", self.RETURN_OUTPUT) 284 | 285 | s = self.output(*args, **kwargs) 286 | if kwargs["print_output"]: 287 | self.writeline(s) 288 | 289 | return s.strip() if kwargs["return_output"] else None 290 | 291 | 292 | class V(Interface): 293 | ''' 294 | print the name = values of any passed in variables 295 | 296 | this prints out the passed in name, the value, and the file:line where the 297 | v() method was called so you can easily find it and remove it later 298 | 299 | :example: 300 | foo = 1 301 | bar = [1, 2, 3] 302 | out.v(foo, bar) 303 | """ prints out: 304 | foo = 1 305 | 306 | bar = 307 | [ 308 | 0: 1, 309 | 1: 2, 310 | 2: 3 311 | ] 312 | 313 | (/file:line) 314 | """ 315 | 316 | :param *args: list, the variables you want to see pretty printed for humans 317 | ''' 318 | value_class = Value 319 | """the default class to use to introspect an input's value""" 320 | 321 | def create_value(self, value, **kwargs): 322 | value_class = kwargs.get("value_class", self.value_class) 323 | return value_class(value, **kwargs) 324 | 325 | def name_value(self, name, body, **kwargs): 326 | name = super().name_value(name, body, **kwargs) 327 | if name: 328 | value = self.create_value(body, **kwargs) 329 | name = value.name_value(name) 330 | return name 331 | 332 | def body_value(self, body, **kwargs): 333 | value = self.create_value(body, **kwargs) 334 | return value.string_value() + "\n" 335 | 336 | def input(self, *args, **kwargs): 337 | call_info = self.reflect.info 338 | if not call_info["args"]: 339 | raise ValueError("you didn't pass any arguments") 340 | 341 | for v in call_info["args"]: 342 | yield v["name"], v["val"] 343 | 344 | 345 | class VS(V): 346 | """ 347 | exactly like v, but doesn't print variable names or file positions 348 | 349 | .. seealso:: ss() 350 | """ 351 | SHOW_META = False 352 | 353 | 354 | class VV(VS): 355 | """alias of VS""" 356 | pass 357 | 358 | 359 | class S(V): 360 | """ 361 | exactly like v() but returns the string instead of printing it out 362 | 363 | since -- 10-15-2015 364 | return -- str 365 | """ 366 | PRINT_OUTPUT = False 367 | RETURN_OUTPUT = True 368 | 369 | 370 | class SS(S): 371 | """exactly like s, but doesn't return variable names or file positions 372 | (useful for logging) 373 | 374 | since -- 10-15-2015 375 | return -- str 376 | """ 377 | SHOW_META = False 378 | 379 | 380 | class X(V): 381 | '''same as v() but calls sys.exit() after printing values 382 | 383 | I just find this really handy for debugging sometimes 384 | 385 | since -- 2013-5-9 386 | https://github.com/Jaymon/pout/issues/50 387 | ''' 388 | def __call__(self, *args, **kwargs): 389 | if not args: 390 | self.reflect.info["args"] = [{ 391 | "name": None, 392 | "val": 'exit at line {}'.format(self.reflect.info["line"]), 393 | }] 394 | 395 | super().__call__(*args, **kwargs) 396 | exit_code = int(kwargs.get("exit_code", kwargs.get("code", 1))) 397 | sys.exit(exit_code) 398 | 399 | 400 | class I(V): 401 | """Print out all class information (properties and methods) of the values 402 | """ 403 | def body_value(self, body, **kwargs): 404 | kwargs.setdefault("SHOW_METHODS", True) 405 | kwargs.setdefault("SHOW_MAGIC", True) 406 | kwargs.setdefault("SHOW_VAL", False) 407 | kwargs.setdefault("SHOW_OBJECT", True) 408 | kwargs.setdefault("SHOW_INSTANCE_ID", True) 409 | kwargs.setdefault("SHOW_INSTANCE_TYPE", True) 410 | kwargs.setdefault("SHOW_SIMPLE_EMPTY", False) 411 | kwargs.setdefault("SHOW_SIMPLE_PREFIX", False) 412 | return super().body_value(body, **kwargs) 413 | 414 | 415 | class VI(I): 416 | """alias of I""" 417 | pass 418 | 419 | 420 | class R(V): 421 | calls = defaultdict(lambda: {"count": 0, "info": {}}) 422 | 423 | @classmethod 424 | def goodbye(cls, instance): 425 | for s, d in cls.calls.items(): 426 | info = d.get("info", {}) 427 | default_c = "{}.{}()".format( 428 | info.get("call_modname", "Unknown"), 429 | info.get("call_funcname", "Unknown"), 430 | ) 431 | c = info.get("call", default_c).strip() 432 | instance.writeline( 433 | "{} called {} times at {}".format(c, d["count"], s) 434 | ) 435 | 436 | def bump(self, count=1): 437 | s = self.path_value() 438 | r_class = type(self) 439 | r_class.calls[s]["count"] += count 440 | 441 | def register(self): 442 | s = self.path_value() 443 | r_class = type(self) 444 | if not r_class.calls: 445 | # https://docs.python.org/3/library/atexit.html 446 | atexit.register(r_class.goodbye, instance=self) 447 | 448 | r_class.calls[s]["info"] = self.reflect.info 449 | 450 | def output(self, *args, **kwargs): 451 | return super().output(*args, **kwargs).strip() 452 | 453 | def __call__(self, *args, **kwargs): 454 | """Similar to pout.v() but gets rid of name and file information so it 455 | can be used in loops and stuff, it will print out where the calls came 456 | from at the end of execution 457 | 458 | this just makes it nicer when you're printing a bunch of stuff each 459 | iteration 460 | 461 | :Example: 462 | for x in range(x): 463 | pout.r(x) 464 | """ 465 | kwargs.setdefault("show_path", False) 466 | super().__call__(*args, **kwargs) 467 | self.register() 468 | self.bump() 469 | 470 | 471 | class VR(R): 472 | """alias of R""" 473 | pass 474 | 475 | 476 | class Sleep(Interface): 477 | def __call__(self, seconds, **kwargs): 478 | '''same as time.sleep(seconds) but prints out where it was called 479 | before sleeping and then again after finishing sleeping 480 | 481 | I just find this really handy for debugging sometimes 482 | 483 | since -- 2017-4-27 484 | 485 | :param seconds: float|int, how many seconds to sleep 486 | ''' 487 | if seconds <= 0.0: 488 | raise ValueError("Invalid seconds {}".format(seconds)) 489 | 490 | self.writeline("Sleeping {} second{} at {}".format( 491 | seconds, 492 | "s" if seconds != 1.0 else "", 493 | self.path_value() 494 | )) 495 | 496 | time.sleep(seconds) 497 | self.writelines(["...Done Sleeping", self.path_value(), ""]) 498 | 499 | 500 | class H(Interface): 501 | ''' 502 | prints "here count" 503 | 504 | example -- 505 | h(1) # here 1 (/file:line) 506 | h() # here line (/file:line) 507 | 508 | count -- integer -- the number you want to put after "here" 509 | ''' 510 | def body_value(self, count, **kwargs): 511 | call_info = self.reflect.info 512 | count = int(count or 0) 513 | return "here {} ".format(count if count > 0 else call_info['line']) 514 | 515 | 516 | class B(Interface): 517 | ''' 518 | create a big text break, you just kind of have to run it and see 519 | 520 | since -- 2013-5-9 521 | 522 | :param *args: mixed, 1-3 arguments 523 | 1 arg = title if string/variable, rows if int 524 | 2 args = title, int 525 | 3 args = title, int, sep 526 | ''' 527 | def input(self, *args, **kwargs): 528 | yield None, args 529 | 530 | def body_value(self, args, **kwargs): 531 | call_info = self.reflect.info 532 | 533 | lines = [] 534 | 535 | title = '' 536 | rows = 1 537 | sep = '*' 538 | 539 | if len(args) == 1: 540 | v = Value(args[0]) 541 | if v.typename in set(['STRING', 'BYTES']): 542 | title = args[0] 543 | 544 | elif v.typename in set(["INT"]): 545 | arg_name = String(self.reflect.info["args"][0]["name"]) 546 | arg_val = String(self.reflect.info["args"][0]["val"]) 547 | if arg_name == arg_val: 548 | rows = int(args[0]) 549 | else: 550 | title = args[0] 551 | 552 | else: 553 | rows = int(args[0]) 554 | 555 | elif len(args) == 2: 556 | title = args[0] 557 | rows = args[1] 558 | 559 | elif len(args) == 3: 560 | title = args[0] 561 | rows = args[1] 562 | sep = String(args[2]) 563 | 564 | if not rows: rows = 1 565 | half_rows = int(math.floor(rows / 2)) 566 | is_even = (rows >= 2) and ((rows % 2) == 0) 567 | 568 | line_len = title_len = 80 569 | if title: 570 | title = ' {} '.format(String(title)) 571 | title_len = len(title) 572 | if title_len > line_len: 573 | line_len = title_len 574 | 575 | for x in range(half_rows): 576 | lines.append(sep * line_len) 577 | 578 | lines.append(title.center(line_len, sep)) 579 | 580 | for x in range(half_rows): 581 | lines.append(sep * line_len) 582 | 583 | else: 584 | for x in range(rows): 585 | lines.append(sep * line_len) 586 | 587 | lines.append('') 588 | return "\n".join(lines) 589 | 590 | 591 | class C(V): 592 | '''kind of like od -c on the command line, basically it dumps each 593 | character and info about that char 594 | 595 | since -- 2013-5-9 596 | 597 | :param *args: tuple, one or more strings to dump 598 | ''' 599 | def body_value(self, arg, **kwargs): 600 | call_info = self.reflect.info 601 | lines = [] 602 | counter = Counter() 603 | arg = String(arg) 604 | counter["total"] = len(arg) 605 | 606 | lines.append('Total Characters: {}'.format(counter['total'])) 607 | for i, c in enumerate(arg, 1): 608 | 609 | line = ['{}.'.format(i)] 610 | if c == '\n': 611 | line.append('\\n') 612 | elif c == '\r': 613 | line.append('\\r') 614 | elif c == '\t': 615 | line.append('\\t') 616 | else: 617 | line.append(c) 618 | 619 | line.append(repr(c.encode(environ.ENCODING))) 620 | 621 | cint = ord(c) 622 | if cint > 65535: 623 | line.append('\\U{:0>8X}'.format(cint)) 624 | else: 625 | line.append('\\u{:0>4X}'.format(cint)) 626 | 627 | if cint < 128: 628 | counter["ascii"] += 1 629 | elif cint < 256: 630 | counter["extended"] += 1 631 | else: 632 | counter["unicode"] += 1 633 | 634 | line.append(unicodedata.name(c, 'UNKNOWN')) 635 | lines.append('\t'.join(line)) 636 | 637 | lines.append("Total: {}, Ascii: {}, extended: {}, unicode: {}".format( 638 | counter['total'], 639 | counter['ascii'], 640 | counter['extended'], 641 | counter['unicode'], 642 | )) 643 | lines.append("") 644 | return "\n".join(lines) 645 | 646 | 647 | class J(V): 648 | """ 649 | dump json 650 | 651 | since -- 2013-9-10 652 | 653 | *args -- tuple -- one or more json strings to dump 654 | """ 655 | def body_value(self, body, **kwargs): 656 | return super().body_value(json.loads(body), **kwargs) 657 | 658 | 659 | class M(Interface): 660 | """ 661 | Print out memory usage at this point in time 662 | 663 | http://docs.python.org/2/library/resource.html 664 | http://stackoverflow.com/a/15448600/5006 665 | http://stackoverflow.com/questions/110259/which-python-memory-profiler-is-recommended 666 | """ 667 | def body_value(self, name, **kwargs): 668 | if not resource: 669 | return self._printstr(["UNSUPPORTED OS\n"]) 670 | 671 | usage = resource.getrusage(resource.RUSAGE_SELF) 672 | # according to the docs, this should give something good but it doesn't 673 | # jive with activity monitor, so I'm using the value that gives me what 674 | # activity monitor gives me 675 | # http://docs.python.org/2/library/resource.html#resource.getpagesize 676 | # (usage[2] * resource.getpagesize()) / (1024 * 1024) 677 | # http://stackoverflow.com/questions/5194057/better-way-to-convert-file-sizes-in-python 678 | rss = 0.0 679 | platform_name = platform.system() 680 | if platform_name == 'Linux': 681 | # linux seems to return KB, while OSX returns B 682 | rss = float(usage[2]) / 1024.0 683 | else: 684 | rss = float(usage[2]) / (1024.0 * 1024.0) 685 | 686 | summary = '' 687 | if name: 688 | summary += "{}: ".format(name) 689 | 690 | summary += "{0} mb\n".format(round(rss, 2)) 691 | return summary 692 | 693 | 694 | class E(Interface): 695 | """Easy exception/error printing 696 | 697 | see e() 698 | since 5-27-2020 699 | 700 | :Example: 701 | with pout.e(): 702 | raise ValueError("foo") 703 | 704 | https://github.com/Jaymon/pout/issues/59 705 | """ 706 | def body_value(self, *args, **kwargs): 707 | lines = traceback.format_exception( 708 | self.exc_type, 709 | self.exc_value, 710 | self.traceback 711 | ) 712 | return self._printstr(lines) 713 | 714 | def __enter__(self): 715 | return self 716 | 717 | def __exit__(self, exc_type, exc_value, traceback): 718 | if exc_type: 719 | self.exc_type = exc_type 720 | self.exc_value = exc_value 721 | self.traceback = traceback 722 | self.writeline(self.output()) 723 | raise exc_value 724 | 725 | def __call__(self, **kwargs): 726 | """ 727 | :returns: context manager 728 | """ 729 | return self 730 | 731 | 732 | class P(Interface): 733 | """this is a context manager for Profiling 734 | 735 | see -- p() 736 | since -- 10-21-2015 737 | """ 738 | 739 | # profiler p() state is held here 740 | stack = [] 741 | 742 | @classmethod 743 | def pop(cls, reflect=None): 744 | instance = cls.stack[-1] 745 | instance.stop(reflect.info) 746 | cls.stack.pop(-1) 747 | return instance 748 | 749 | def start(self, name, call_info, **kwargs): 750 | self.start = time.time() 751 | self.name = name 752 | self.start_call_info = call_info 753 | self.kwargs = kwargs 754 | type(self).stack.append(self) 755 | 756 | def stop(self, call_info=None): 757 | pr_class = type(self) 758 | name = self.name 759 | if len(pr_class.stack) > 0: 760 | found = False 761 | ds = [] 762 | for d in pr_class.stack: 763 | if self is d: 764 | found = True 765 | break 766 | 767 | else: 768 | ds.append(d) 769 | 770 | if found and ds: 771 | name = ' > '.join((d.name for d in ds)) 772 | name += ' > {}'.format(self.name) 773 | 774 | self.stop_call_info = call_info or self.reflect.info 775 | self.name = name 776 | self.stop = time.time() 777 | self.elapsed = self.get_elapsed(self.start, self.stop, 1000.00, 1) 778 | self.total = "{:.1f} ms".format(self.elapsed) 779 | 780 | def __enter__(self): 781 | return self 782 | 783 | def __exit__(self, *args, **kwargs): 784 | pr_class = type(self) 785 | for i in range(len(pr_class.stack)): 786 | if self is pr_class.stack[i]: 787 | self.stop() 788 | pr_class.stack.pop(i) 789 | break 790 | 791 | self.finish() 792 | 793 | def finish(self): 794 | self.writeline(self.output(**self.kwargs)) 795 | 796 | def body_value(self, *args, **kwargs): 797 | s = "" 798 | start_call_info = self.start_call_info 799 | stop_call_info = self.stop_call_info 800 | 801 | summary = [] 802 | summary.append("{} - {}".format(self.name, self.total)) 803 | summary.append(" start: {} ({}:{})".format( 804 | self.start, 805 | self._get_path(start_call_info['file']), 806 | start_call_info['line'] 807 | )) 808 | 809 | if stop_call_info: 810 | summary.append(" stop: {} ({}:{})".format( 811 | self.stop, 812 | self._get_path(stop_call_info['file']), 813 | stop_call_info['line'] 814 | )) 815 | 816 | else: 817 | summary.append(" stop: {}".format(self.stop)) 818 | 819 | return "\n".join(summary) 820 | 821 | def get_elapsed(self, start, stop, multiplier, rnd): 822 | return round(abs(stop - start) * float(multiplier), rnd) 823 | 824 | def __call__(self, name="", **kwargs): 825 | ''' 826 | really quick and dirty profiling 827 | 828 | you start a profile by passing in name, you stop the top profiling by 829 | not passing in a name. You can also call this method using a with 830 | statement 831 | 832 | This is for when you just want to get a really back of envelope view of 833 | how your fast your code is, super handy, not super accurate 834 | 835 | since -- 2013-5-9 836 | example -- 837 | p("starting profile") 838 | time.sleep(1) 839 | p() # stop the "starting profile" session 840 | 841 | # you can go N levels deep 842 | p("one") 843 | p("two") 844 | time.sleep(0.5) 845 | p() # stop profiling of "two" 846 | time.sleep(0.5) 847 | p() # stop profiling of "one" 848 | 849 | with pout.p("three"): 850 | time.sleep(0.5) 851 | 852 | name -- string -- pass this in to start a profiling session 853 | return -- context manager 854 | ''' 855 | kwargs.setdefault("show_path", False) 856 | if name: 857 | self.start(name, self.reflect.info, **kwargs) 858 | instance = self 859 | else: 860 | instance = type(self).pop(self.reflect) 861 | instance.finish() 862 | 863 | return instance 864 | 865 | 866 | class L(Interface): 867 | """Logging context manager used in pout.l() 868 | 869 | This will turn logging to the stderr on for everything inside the with 870 | block 871 | 872 | :Example: 873 | with LoggingInterface(): 874 | logger.debug("This will print on screen even if logging is off") 875 | logger.debug("this will not print if logging is off") 876 | 877 | similar to: 878 | https://github.com/python/cpython/blob/d918bbda4bb201c35d1ded3dde686d8b00a91851/Lib/unittest/case.py#L297 879 | """ 880 | @property 881 | def loggers(self): 882 | """Return all the loggers that should be activated""" 883 | ret = [] 884 | if self.logger_name: 885 | if isinstance(self.logger_name, logging.Logger): 886 | ret.append((self.logger_name.name, self.logger_name)) 887 | else: 888 | ret.append( 889 | (self.logger_name, logging.getLogger(self.logger_name)) 890 | ) 891 | 892 | else: 893 | ret = list(logging.Logger.manager.loggerDict.items()) 894 | ret.append(("root", logging.getLogger())) 895 | return ret 896 | 897 | def __enter__(self): 898 | old_loggers = collections.defaultdict(dict) 899 | for logger_name, logger in self.loggers: 900 | try: 901 | old_loggers[logger_name]["handlers"] = logger.handlers[:] 902 | old_loggers[logger_name]["level"] = logger.level 903 | old_loggers[logger_name]["propagate"] = logger.propagate 904 | 905 | handler = logging.StreamHandler(stream=sys.stderr) 906 | #handler.setFormatter(formatter) 907 | logger.handlers = [handler] 908 | logger.setLevel(self.level) 909 | logger.propagate = False 910 | 911 | except AttributeError: 912 | pass 913 | 914 | 915 | self.old_loggers = old_loggers 916 | return self 917 | 918 | def __exit__(self, *args, **kwargs): 919 | for logger_name, logger_dict in self.old_loggers.items(): 920 | logger = logging.getLogger(logger_name) 921 | for name, val in logger_dict.items(): 922 | if name == "level": 923 | logger.setLevel(val) 924 | else: 925 | setattr(logger, name, val) 926 | 927 | def __call__(self, logger_name="", level=logging.DEBUG, **kwargs): 928 | """see Logging class for details, this is just the method wrapper 929 | around Logging 930 | 931 | :Example: 932 | # turn on logging for all loggers 933 | with pout.l(): 934 | # do stuff that produces logs and see it prints to stderr 935 | 936 | # turn on logging just for a specific logger 937 | with pout.l("name"): 938 | # "name" logger will print to stderr, all other loggers will 939 | # act as configured 940 | 941 | :param logger_name: string|Logger, the logger name you want to print to 942 | stderr 943 | :param level: string|int, the logging level the logger should be set 944 | at, this defaults to logging.DEBUG 945 | """ 946 | self.logger_name = logger_name 947 | 948 | if isinstance(level, basestring): 949 | self.level = logging._nameToLevel[level.upper()] 950 | 951 | else: 952 | self.level = level 953 | 954 | return self 955 | 956 | 957 | class T(Interface): 958 | """Print a backtrace from the location of where this was called""" 959 | 960 | call_class = Call 961 | 962 | def body_value(self, *args, **kwargs): 963 | name = kwargs.get("name", "") 964 | frames = kwargs["frames"] 965 | inspect_packages = kwargs.get("inspect_packages", False) 966 | depth = kwargs.get("depth", 0) 967 | 968 | calls = self._get_backtrace( 969 | frames=frames, 970 | inspect_packages=inspect_packages, 971 | depth=depth 972 | ) 973 | return "".join(calls) 974 | 975 | def _get_backtrace(self, frames, inspect_packages=False, depth=0): 976 | """Get a nicely formatted backtrace 977 | 978 | since -- 7-6-12 979 | 980 | :param frames: list, the frame_tuple frames to format 981 | :param inpsect_packages: bool, by default, this only prints code of 982 | packages that are not in the pythonN directories, that cuts out a 983 | lot of the noise, set this to True if you want a full stacktrace 984 | :param depth: int, how deep you want the stack trace to print (ie, if 985 | you only care about the last three calls, pass in depth=3 so you 986 | only get the last 3 rows of the stack) 987 | 988 | :returns: list, each line will be a nicely formatted entry of the 989 | backtrace 990 | """ 991 | calls = [] 992 | 993 | for index, f in enumerate(frames, 1): 994 | # https://stackoverflow.com/a/2011168/5006 995 | called_module = None 996 | if f[0].f_back: 997 | called_module = sys.modules[f[0].f_back.f_globals["__name__"]] 998 | called_func = f[3] 999 | call = self.call_class(called_module, called_func, f) 1000 | s = self._get_call_summary( 1001 | call, 1002 | inspect_packages=inspect_packages, 1003 | index=index 1004 | ) 1005 | calls.append(s) 1006 | 1007 | if depth and (index > depth): 1008 | break 1009 | 1010 | # reverse the order on return so most recent is on the bottom 1011 | return calls[::-1] 1012 | 1013 | def _get_call_summary(self, call, index=0, inspect_packages=True): 1014 | """Get a call summary 1015 | 1016 | a call summary is a nicely formatted string synopsis of the call 1017 | 1018 | handy for backtraces 1019 | 1020 | since -- 7-6-12 1021 | 1022 | :param call_info: dict, the dict returned from _get_call_info() 1023 | :param index: int, set to something above 0 if you would like the 1024 | summary to be numbered 1025 | :param inspect_packages: bool, set to True to get the full format even 1026 | for system frames 1027 | :returns: str 1028 | """ 1029 | call_info = call.info 1030 | inspect_regex = re.compile(r'[\\/]python\d(?:\.\d+)?', re.I) 1031 | INDENT_STRING = environ.INDENT_STRING 1032 | 1033 | # truncate the filepath if it is super long 1034 | f = call_info['file'] 1035 | if len(f) > 75: 1036 | f = "{}...{}".format(f[0:30], f[-45:]) 1037 | 1038 | if inspect_packages or not inspect_regex.search(call_info['file']): 1039 | s = "{}:{}\n\n{}\n".format( 1040 | f, 1041 | call_info['line'], 1042 | String(call_info['call']).dedent().indent(INDENT_STRING, 1) 1043 | ) 1044 | 1045 | else: 1046 | s = "{}:{}\n".format( 1047 | f, 1048 | call_info['line'] 1049 | ) 1050 | 1051 | if index > 0: 1052 | s = "{:02d} - {}".format(index, s) 1053 | 1054 | return s 1055 | 1056 | def __call__(self, inspect_packages=False, depth=0, **kwargs): 1057 | ''' 1058 | print a backtrace 1059 | 1060 | since -- 7-6-12 1061 | 1062 | :param inpsect_packages: bool, by default, this only prints code of 1063 | packages that are not in the pythonN directories, that cuts out a 1064 | lot of the noise, set this to True if you want a full stacktrace 1065 | :param depth: int, how deep you want the stack trace to print (ie, if 1066 | you only care about the last three calls, pass in depth=3 so you 1067 | only get the last 3 rows of the stack) 1068 | ''' 1069 | try: 1070 | frames = inspect.stack() 1071 | kwargs["frames"] = frames[1:] 1072 | kwargs["inspect_packages"] = inspect_packages 1073 | kwargs["depth"] = depth 1074 | super().__call__(**kwargs) 1075 | 1076 | finally: 1077 | del frames 1078 | 1079 | 1080 | class Tofile(Interface): 1081 | def __enter__(self): 1082 | self.orig_stream = self.kwargs["pout_module"].stream 1083 | self.kwargs["pout_module"].stream = self.stream 1084 | return self 1085 | 1086 | def __exit__(self, *args, **kwargs): 1087 | self.kwargs["pout_module"].stream = self.orig_stream 1088 | 1089 | def __call__(self, path="", **kwargs): 1090 | """Instead of printing to a screen print to a file 1091 | 1092 | :Example: 1093 | with pout.tofile("/path/to/file.txt"): 1094 | # all pout calls in this with block will print to file.txt 1095 | pout.v("a string") 1096 | pout.b() 1097 | pout.h() 1098 | 1099 | :param path: str, a path to the file you want to write to 1100 | """ 1101 | if not path: 1102 | path = os.path.join( 1103 | os.getcwd(), 1104 | "{}.txt".format(self.module_name().upper()) 1105 | ) 1106 | 1107 | self.path = path 1108 | self.kwargs = kwargs 1109 | self.stream = FileStream(path) 1110 | 1111 | return self 1112 | 1113 | 1114 | class F(Tofile): 1115 | """alias function name of Tofile""" 1116 | pass 1117 | 1118 | 1119 | # class Watchcalls(Interface): 1120 | # """Watch all the method calls of a given object""" 1121 | # def __call__(self, obj, **kwargs): 1122 | # 1123 | # if isinstance(obj, type): 1124 | # class Wrapper(obj): 1125 | # def __getattribute__(_self, name): 1126 | # s = self.output(name) 1127 | # self.writeline(s) 1128 | # return super().__getattribute__(name) 1129 | # 1130 | # return Wrapper 1131 | # 1132 | # else: 1133 | # parent_class = obj.__class__ 1134 | # 1135 | # class Wrapper(obj.__class__): 1136 | # def __init__(self, interface, obj): 1137 | # self.interface = interface 1138 | # self.obj = obj 1139 | # 1140 | # def __getattribute__(self, name): 1141 | # s = self.interface.output(name) 1142 | # self.interface.writeline(s) 1143 | # return getattr(self.obj, name) 1144 | # 1145 | # return Wrapper(self, obj) 1146 | 1147 | --------------------------------------------------------------------------------