├── requirements.txt ├── tests ├── __init__.py ├── tests_exceptions.py └── tests_core.py ├── .github ├── demo_inputy.gif ├── demo_printy.gif ├── escape_printy.png ├── inputy_example.png ├── printy_helpme.png ├── printy_f_strings.png ├── printy_from_file.png ├── printy_no_format.png ├── background_printy.png ├── printy_raw_format.png ├── printy_COLORS_FORMATS.png ├── printy_global_bold_blue.png ├── printy_inline_red_italic.png ├── printy_changing_predefined.png ├── inputy_max_digits_max_decimals.png ├── printy_override_inline_with_global.png ├── printy_pretty_dict_two_indentation.png └── printy_pretty_dict_four_indentation.png ├── printy ├── __main__.py ├── exceptions.py ├── __init__.py ├── helpme.py ├── flags.py └── core.py ├── .travis.yml ├── LICENSE ├── setup.py ├── CHANGELOG.md ├── .all-contributorsrc ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/demo_inputy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/demo_inputy.gif -------------------------------------------------------------------------------- /.github/demo_printy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/demo_printy.gif -------------------------------------------------------------------------------- /.github/escape_printy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/escape_printy.png -------------------------------------------------------------------------------- /.github/inputy_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/inputy_example.png -------------------------------------------------------------------------------- /.github/printy_helpme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_helpme.png -------------------------------------------------------------------------------- /.github/printy_f_strings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_f_strings.png -------------------------------------------------------------------------------- /.github/printy_from_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_from_file.png -------------------------------------------------------------------------------- /.github/printy_no_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_no_format.png -------------------------------------------------------------------------------- /.github/background_printy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/background_printy.png -------------------------------------------------------------------------------- /.github/printy_raw_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_raw_format.png -------------------------------------------------------------------------------- /.github/printy_COLORS_FORMATS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_COLORS_FORMATS.png -------------------------------------------------------------------------------- /.github/printy_global_bold_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_global_bold_blue.png -------------------------------------------------------------------------------- /.github/printy_inline_red_italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_inline_red_italic.png -------------------------------------------------------------------------------- /.github/printy_changing_predefined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_changing_predefined.png -------------------------------------------------------------------------------- /.github/inputy_max_digits_max_decimals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/inputy_max_digits_max_decimals.png -------------------------------------------------------------------------------- /.github/printy_override_inline_with_global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_override_inline_with_global.png -------------------------------------------------------------------------------- /.github/printy_pretty_dict_two_indentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_pretty_dict_two_indentation.png -------------------------------------------------------------------------------- /.github/printy_pretty_dict_four_indentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edraobdu/printy/HEAD/.github/printy_pretty_dict_four_indentation.png -------------------------------------------------------------------------------- /printy/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | renders the helper text that shows some of the available functions 3 | by running python -m printy 4 | """ 5 | 6 | from . import printy 7 | 8 | if __name__ == '__main__': 9 | from .helpme import helpme 10 | printy(helpme) 11 | -------------------------------------------------------------------------------- /printy/exceptions.py: -------------------------------------------------------------------------------- 1 | #### Printy 2 | 3 | 4 | class InvalidFlag(Exception): 5 | """ raised when an invalid flag is passed to the 'printy' object""" 6 | 7 | def __init__(self, flag): 8 | self.flag = flag 9 | 10 | def __str__(self): 11 | return "'%s' is not a valid flag" % self.flag 12 | 13 | 14 | #### Inputy 15 | 16 | 17 | class InvalidInputType(Exception): 18 | """ raised when an invalid 'type' is passed to the 'inputy' function""" 19 | 20 | def __init__(self, input_type): 21 | self.input_type = input_type 22 | 23 | def __str__(self): 24 | return "'%s' is not a valid type" % self.input_type 25 | -------------------------------------------------------------------------------- /tests/tests_exceptions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from printy.exceptions import InvalidFlag, InvalidInputType 3 | 4 | 5 | class TestExceptions(unittest.TestCase): 6 | """ Test Case for exceptions """ 7 | 8 | def setUp(self): 9 | self.invalid_flag = "l" 10 | self.invalid_flag_error = "'%s' is not a valid flag" % self.invalid_flag 11 | 12 | self.invalid_input_type = 'path' 13 | self.invalid_input_type_error = "'%s' is not a valid type" % self.invalid_input_type 14 | 15 | def test_invalid_flag_str(self): 16 | """ test that the exception InvalidFlag returns the expected text """ 17 | error = InvalidFlag(self.invalid_flag) 18 | 19 | self.assertEqual(str(error), self.invalid_flag_error) 20 | 21 | def test_invalid_input_type_str(self): 22 | """ test that the exception InvalidInputType returns the expected text """ 23 | error = InvalidInputType(self.invalid_input_type) 24 | 25 | self.assertEqual(str(error), self.invalid_input_type_error) 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | deploy: 4 | provider: pypi 5 | user: "__token__" 6 | password: 7 | secure: "tEAnydGtzCP5tzPkHpVUo0nzM8xjaAvNg/6ynHNyr6B78n33VX08xgVSzmih3bIz0ZqBDWi6v2tMNGMMO0LYtbjavvdCEfmzmOnFdPikZr8OJrWHiiup2edPzUArWPzsSX0lgzwmcOFV2t6IKcE0dDRvx6g3ETjs23iY4NIOtqXiNtKh19IqDlAX31S0ps8m9wPuCIm4HFbkwlJaQ4IKgBNSLMShfrZuBxSH8V6KNqfn+MDczmunfjiIDjwDYWvdxXgn+JqVbPEZTF40DzgQlSa3gOeTej2Ps+Fl9Yl5rfTqW4ndCaHtraWt7Yj3No5OWNjE/e3Nduw2RRCK+MhBGiEWDG3T0YtgDLkJ2PxnUKUq56Sh4fJm0VaFX6MNSptqrtzkqZnSFa7hQKh0fKaNK1nrJZ4vsrpLCV71LVHltwjfM77AvmSJuKMGip1DPJgF5hXm+cv2NlNmNKfNYvz6smBdr/CjpyutSZfzJhiBQimGvZigyY0xF/eNIRtVAV2UnXDTpAak4OcUbhzChakX1H6FHSdrBMBewEM8G9Hl5XctM6b9e7sbC44Tscwga1LfBsVl7pKryzqqrj+wHErZM7efuTr3CPTIk5Sh87K5wAB1HzoLA3k6sRAAymsvQLuHV867RL03QYRE10+JJbaS+3Nk1nKJh+ZptYKjcD8JVxk=" 8 | on: 9 | branch: master 10 | distributions: "sdist bdist_wheel" 11 | skip_existing: true 12 | install: 13 | - pip install coverage==5.0.4 14 | script: 15 | - coverage run -m unittest discover 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Edgardo Obregón 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 | -------------------------------------------------------------------------------- /printy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Printy extends the capabilities of the builtins print() and input(), allowing 4 | us to add some colors and formats to the text, either globally or inline with 5 | an friendly and intuitive syntax, reading from a file and adding some validation 6 | to the data the final user is entering by the console 7 | 8 | """ 9 | 10 | from .core import Printy 11 | from .flags import Flags 12 | 13 | __version__ = "2.2.0" 14 | 15 | __all__ = ['raw_format', 'printy', 'inputy', 'COLORS', 'FORMATS'] 16 | 17 | printy_instance = Printy() 18 | 19 | # If user just want to get the formatted text with the ANSI escape sequences 20 | raw_format = printy_instance.get_formatted_text 21 | 22 | # Main function to extend print() functionality 23 | printy = printy_instance.format 24 | 25 | # Main function to extend input() functionality 26 | inputy = printy_instance.format_input 27 | 28 | # Escaping function for untrusted sources 29 | escape = printy_instance.escape 30 | 31 | # shortcut to get a list of the available flags and formats 32 | available_flags = Flags.get_flags().keys() 33 | COLORS = list(filter(lambda c: c.islower(), available_flags)) 34 | FORMATS = list(filter(lambda f: f.isupper(), available_flags)) 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | from printy import __version__ 6 | 7 | readme_path = os.path.join(os.path.dirname(__file__), "README.md") 8 | with open(readme_path) as fh: 9 | long_description = fh.read() 10 | 11 | setup( 12 | name="printy", 13 | version=__version__, 14 | url="https://github.com/edraobdu/printy", 15 | author="Edgardo Obregón", 16 | author_email="edraobdu@gmail.com", 17 | description="Colorize the print statement by global or inline flags", 18 | license="MIT", 19 | long_description=long_description, 20 | long_description_content_type="text/markdown", 21 | packages=find_packages(), 22 | classifiers=[ 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.5", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | ], 34 | python_requires='>=3.5' 35 | ) 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.2.0] - 2021-02-28 9 | 10 | ### Added 11 | 12 | - Added escape function 13 | - Added background color 14 | 15 | ## [2.1.0] - 2020-06-13 16 | 17 | ### Added 18 | 19 | - Added pretty printing objects 20 | - Added objects str method correctly printed out 21 | - Added max_digits restriction on inputy 22 | - Added max_decimals restriction on inputy 23 | - Added Dim compatibility again with flag 'D' 24 | 25 | ## [2.0.1] - 2020-05-01 26 | 27 | ### Fixed 28 | 29 | - Fixed error on rendering default value for inputy's options 30 | 31 | 32 | ## [2.0.0] - 2020-04-27 33 | 34 | ### Added 35 | 36 | - Added high and low intensity flag colors 37 | - Added list of options for inputy 38 | - Added render_options on inputy 39 | - Added default parameter on inputy 40 | 41 | ### Changed 42 | 43 | - Changes options parameter on inputy 44 | - Changed 'p' flag from Predefined color to Purple Color 45 | 46 | ### Removed 47 | 48 | - Removed 'D' flag (Dim), not widely supported 49 | 50 | 51 | ## [1.2.1] - 2020-02-14 52 | 53 | ### Fixed 54 | 55 | - Fixed type 'bool' values 56 | 57 | ## [1.2.0] - 2020-01-13 58 | 59 | ### Added 60 | 61 | - Added support for Windows, printy is now cross-platform 62 | 63 | ## [1.1.0] - 2020-01-13 64 | 65 | ### Added 66 | 67 | - Added new function 'inputy' 68 | 69 | ## [1.0.0] - 2020-04-11 70 | 71 | ### Added 72 | 73 | - First official release 74 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "edraobdu", 10 | "name": "Edgardo Obregón", 11 | "avatar_url": "https://avatars3.githubusercontent.com/u/31775663?v=4", 12 | "profile": "https://github.com/edraobdu", 13 | "contributions": [ 14 | "code", 15 | "test", 16 | "example", 17 | "ideas", 18 | "maintenance", 19 | "doc", 20 | "bug" 21 | ] 22 | }, 23 | { 24 | "login": "farahduk", 25 | "name": "farahduk", 26 | "avatar_url": "https://avatars3.githubusercontent.com/u/15660335?v=4", 27 | "profile": "https://github.com/farahduk", 28 | "contributions": [ 29 | "ideas", 30 | "code", 31 | "maintenance" 32 | ] 33 | }, 34 | { 35 | "login": "mihirs16", 36 | "name": "Mihir Singh", 37 | "avatar_url": "https://avatars3.githubusercontent.com/u/44063783?v=4", 38 | "profile": "https://github.com/mihirs16", 39 | "contributions": [ 40 | "test", 41 | "code" 42 | ] 43 | }, 44 | { 45 | "login": "musicprogram", 46 | "name": "musicprogram", 47 | "avatar_url": "https://avatars1.githubusercontent.com/u/7810348?v=4", 48 | "profile": "https://soundcloud.com/lalalaaalala", 49 | "contributions": [ 50 | "userTesting" 51 | ] 52 | }, 53 | { 54 | "login": "Tams-Tams", 55 | "name": "Tanmay", 56 | "avatar_url": "https://avatars.githubusercontent.com/u/63205558?v=4", 57 | "profile": "https://github.com/Tams-Tams", 58 | "contributions": [ 59 | "ideas" 60 | ] 61 | }, 62 | { 63 | "login": "obentham", 64 | "name": "Oliver Bentham", 65 | "avatar_url": "https://avatars.githubusercontent.com/u/22358748?v=4", 66 | "profile": "https://github.com/obentham", 67 | "contributions": [ 68 | "userTesting" 69 | ] 70 | } 71 | ], 72 | "contributorsPerLine": 7, 73 | "projectName": "printy", 74 | "projectOwner": "edraobdu", 75 | "repoType": "github", 76 | "repoHost": "https://github.com", 77 | "skipCi": true 78 | } 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | .vscode/settings.json 133 | -------------------------------------------------------------------------------- /printy/helpme.py: -------------------------------------------------------------------------------- 1 | 2 | helpme = """ 3 | ######################################################### 4 | ######################################################### 5 | 6 | [bBU]PRINTY@ 7 | 8 | Printy lets you colorize and apply some standard formats to your text with 9 | an intuitive and friendly API based on flags to specify the formats. You can 10 | either apply a global format or inline formats to specific parts of your text! 11 | 12 | [nB]Let's get started!@ 13 | 14 | [B]COLORS@ 15 | 16 | [gH]#### Gray Scale@ 17 | [k]'k' -> Applies a black color to the text@ 18 | [g]'g' -> Applies a grey color to the text@ 19 | [w]'w' -> Applies a white color to the text@ 20 | 21 | [rH]#### Red Scale@ 22 | [ Applies a darkred color to the text@ 23 | [r]'r' -> Applies a red color to the text@ 24 | [r>]'r>' -> Applies a lightred color to the text@ 25 | 26 | [nH]#### Green Scale@ 27 | [ Applies a darkgreen color to the text@ 28 | [n]'n' -> Applies a green color to the text@ 29 | [n>]'n>' -> Applies a lightgreen color to the text@ 30 | 31 | [yH]#### Yellow Scale@ 32 | [ Applies a darkyellow color to the text@ 33 | [y]'y' -> Applies a yellow color to the text@ 34 | [y>]'y>' -> Applies a lightyellow color to the text@ 35 | 36 | [bH]#### Blue Scale@ 37 | [ Applies a darkblue color to the text@ 38 | [b]'b' -> Applies a blue color to the text@ 39 | [b>]'b>' -> Applies a lightblue color to the text@ 40 | 41 | [mH]#### Magenta Scale@ 42 | [ Applies a darkmagenta color to the text@ 43 | [m]'m' -> Applies a magenta color to the text@ 44 | [m>]'m>' -> Applies a lightmagenta color to the text@ 45 | 46 | [cH]#### Cyan Scale@ 47 | [ Applies a darkcyan color to the text@ 48 | [c]'c' -> Applies a cyan color to the text@ 49 | [c>]'c>' -> Applies a lightcyan color to the text@ 50 | 51 | [oH]#### Orange Scale@ 52 | [ Applies a darkorange color to the text@ 53 | [o]'o' -> Applies a orange color to the text@ 54 | [o>]'o>' -> Applies a lightorange color to the text@ 55 | 56 | [pH]#### Purple Scale@ 57 | [ Applies a darkpurple color to the text@ 58 | [p]'p' -> Applies a purple color to the text@ 59 | [p>]'p>' -> Applies a lightpurple color to the text@ 60 | 61 | [B]FORMATS@ 62 | 63 | [B]'B' -> Applies a bold font weight to the text@ 64 | [U]'U' -> Applies an underline to the text@ 65 | [I]'I' -> Applies an italic font type to the text@ 66 | [H]'H' -> Highlights the text@ 67 | [S]'S' -> crosses out the text, aka Strike@ 68 | [D]'S' -> Dim effect@ 69 | 70 | 71 | [B]HOW TO USE IT?@ 72 | 73 | First import printy: 74 | 75 | [n] >>>@ [>>@ printy([n>]'Some text to be formatted'@[]'b'@) 80 | [b]Some text to be formatted@ 81 | 82 | [n] >>>@ printy([n>]'Some text to be formatted'@[]'bHI'@) 83 | [bHI]Some text to be formatted@ 84 | 85 | Or as Inline format, use the \[\] to specify the flags, and the \@ to finish 86 | the format for a specific section: 87 | 88 | [n] >>>@ printy([n>]'\[r\]Red\@ Default Color \[yH\]Yellow Highlighted\@ Default Color'@) 89 | [r]Red@ Default Color [yH]Yellow Highlighted@ Default Color 90 | 91 | You can always override the whole format with a global flag: 92 | 93 | [n] >>>@ printy([n>]'\[r\]Red\@ Default Color \[yH\]Yellow Highlighted\@ Default Color'@[]'b'@) 94 | [b]Red Default Color Yellow Highlighted Default Color@ 95 | 96 | Or you can change only the predefined color: 97 | 98 | [n] >>>@ printy([n>]'\[r\]Red\@ Default Color \[yH\]Yellow Highlighted\@ Default Color'@[]'b'@) 99 | [r]Red@ [b]Default Color@ [yH]Yellow Highlighted@ [b]Default Color@ 100 | 101 | [n] >>>@ printy([n>]'\[r\]Red\@ Default Color \[yH\]Yellow Highlighted\@ Default Color'@[]'nBIU'@) 102 | [r]Red@ [nBIU]Default Color@ [yH]Yellow Highlighted@ [nBIU]Default Color@ 103 | 104 | If you need to use one of the special characters ('\[', '\]', '\@'), simply escape them 105 | 106 | [n] >>>@ printy([n>]'\[B\]\\\[myemail\\\@mydomain.com\\\]\@'@) 107 | [B]\[myemail\@mydomain.com\]@ 108 | 109 | Now you can do stuffs like: 110 | 111 | [n] >>>@ text = [n>]'Hello world, python is awesome!!!'@ 112 | [n] >>>@ printy(text.replace([n>]'python'@[]'\[r\]python\@'@)) 113 | Hello world, [r]python@ is awesome 114 | 115 | Or some html highlighting: 116 | 117 | [n] >>>@ html = ( 118 | [n] ...@ [n>]'
\]"active"\@ \[p\]id=\@\[n>\]"my-div"\@>'@ 119 | [n] ...@ [n>]' \]"extra-data"\@>\[p\]Some text\@'@ 120 | [n] ...@ [n>]'
'@) 121 | [n] >>>@ printy(html[]'y>'@) 122 | [y>]]'active'@ id=[n>]'my-div'@[y>]>@ 123 | [y>]]'extra-data'@[y>]>@Some text[y>]@ 124 | [y>]@ 125 | 126 | Or, you can use it with python's formatting strings as well: 127 | 128 | [n] >>>@ minutes = [c>]60@ 129 | [n] >>>@ printy([n>]f'A day has \[y\]@[]24@[]\@ minutes'@) 130 | A day has [y]1400@ minutes 131 | 132 | [B]What about @[p>B]input@()? 133 | 134 | printy comes with a wrapper for the python built-in [p>]input@() function 135 | 136 | [n] >>>@ [' 120 | # characters respectively. Dark colors have the form: , i.e. b> 122 | potential_flag = [] 123 | flags = flags.replace(' ', '') # remove white-spaces 124 | # We use inspect so we can check the following character also 125 | 126 | # New in 2.2 127 | # Extract the background, teh chars on brackets {} 128 | bg_regex = '[a-zA-Z<>]{0,}(?P{[a-zA-Z<>]{0,}})[a-zA-Z<>]{0,}' 129 | matched_bg = re.match(bg_regex, flags) 130 | 131 | if matched_bg is not None: 132 | bg = matched_bg.groupdict().get('background') 133 | if bg: 134 | # Remove the background from the flags, so we end up 135 | # with the foreground flasg only 136 | flags = flags.replace(bg, '') 137 | # remove brackets from background 138 | bg = bg[1:-1] 139 | # bg now can be an empty string if there's nothing between the brackets {} 140 | if bg: 141 | if bg not in available_flags: 142 | raise InvalidFlag(bg) 143 | if hasattr(cls, available_flags[bg]): 144 | flags_values.append(cls.get_bg_value(available_flags, bg)) 145 | 146 | for f in range(len(flags)): 147 | flag = flags[f] 148 | 149 | if flag == '<': 150 | potential_flag.append(flag) 151 | continue 152 | elif flag == '>': 153 | potential_flag.append(flag) 154 | flag = ''.join(potential_flag) 155 | potential_flag.clear() 156 | else: 157 | try: 158 | next_flag = flags[f + 1] 159 | except IndexError: 160 | # the last one 161 | next_flag = None 162 | 163 | if next_flag == '>': 164 | potential_flag.append(flag) 165 | continue 166 | else: 167 | potential_flag.append(flag) 168 | flag = ''.join(potential_flag) 169 | potential_flag.clear() 170 | 171 | if flag not in available_flags: 172 | raise InvalidFlag(flag) 173 | else: 174 | if hasattr(cls, available_flags[flag]): 175 | flags_values.append(cls.get_fg_value(available_flags, flag)) 176 | 177 | return flags_values 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Printy 2 | 3 | ![Travis (.org)](https://img.shields.io/travis/edraobdu/printy?logo=travis&style=flat-square) 4 | ![Codecov](https://img.shields.io/codecov/c/gh/edraobdu/printy?logo=codecov&style=flat-square) 5 | ![PyPI](https://img.shields.io/pypi/v/printy?style=flat-square) 6 | ![PyPI - Wheel](https://img.shields.io/pypi/wheel/printy?style=flat-square) 7 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/printy?logo=python&logoColor=blue&style=flat-square) 8 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 9 | ![PyPI - License](https://img.shields.io/pypi/l/printy?style=flat-square) 10 | 11 | Printy is a **light** and **cross-platform** library that extends the functionalities of the 12 | built-in functions ```print()``` and ```input()```. Printy stands out for its 13 | simplicity and for being and easy to use library, it lets you colorize and apply some standard formats 14 | to your text with an intuitive and friendly API based on **flags**. 15 | 16 | ![Printy demo](.github/demo_printy.gif) 17 | 18 | ![Inputy Demo](.github/demo_inputy.gif) 19 | 20 | _NOTE: Printy manages the most common and simple tasks when it comes to print 21 | text and to validate some input. If you want to have more control over the 22 | console output check out **[Rich](https://github.com/willmcgugan/rich)** by @willmcgugan, 23 | an amazing library that let's you do much more cool things!!_ 24 | 25 | ## Table of Contents 26 | 27 | 1. [Installation](#installation) 28 | 2. [How to use it?](#how-to-use-it) 29 | 1. [Using global flags](#using-global-flags) 30 | 2. [Using inline flags](#using-inline-flags) 31 | 3. [Untrusted sources](#untrusted-sources) 32 | 4. [Background Colors](#background-colors) 33 | 3. [What about input()?](#what-about-input) 34 | 4. [Curious?](#curious) 35 | 5. [API](#api) 36 | 1. [printy()](#printy) 37 | 2. [inputy()](#inputy) 38 | 3. [List 1: flags](#list-1-flags) 39 | 4. [List 2: types](#list-2-types) 40 | 5. [List 2: conditions](#list-3-conditions) 41 | 6. [Changelog](#changelog) 42 | 7. [Dependencies](#dependencies) 43 | 8. [Contributing](#contributing) 44 | 9. [Contributors](#contributors-) 45 | 46 | ## Installation 47 | 48 | you can either clone this repository or install it via pip 49 | ```python 50 | pip install printy 51 | ``` 52 | 53 | ## How to use it? 54 | 55 | Once you install printy, you can find a short but concise documentation about the 56 | available flags and the syntax by running the following command on your console: 57 | ```python 58 | python -m printy 59 | ``` 60 | 61 | This will print out some instructions right away. 62 | 63 | ![Printy Help me](.github/printy_helpme.png) 64 | 65 | #### Using global flags 66 | 67 | First of all, import printy: 68 | ```python 69 | from printy import printy 70 | ``` 71 | 72 | Printy is still a 'print' statement, so you can use it as it is: 73 | ```python 74 | printy("text with no format") 75 | ``` 76 | 77 | ![Printy no format](.github/printy_no_format.png) 78 | 79 | You can use a global set of flags to specify a format you want to apply to the text, 80 | let's say we want to colorize a text with a bold blue and also adding an underline: 81 | ```python 82 | printy("Text with a bold blue color and underlined", 'bBU') 83 | ``` 84 | ![Printy global bold blue](.github/printy_global_bold_blue.png) 85 | 86 | #### Using inline flags 87 | Although applying a global format is interesting, it is not as much as applying 88 | some specific format to some section of the text only. For that, printy uses a 89 | intuitive syntax to accomplish that goal. Use the [] to specify the flags to use 90 | for formatting the text, right before the text, and the @ to finish the formatting 91 | section: 92 | ```python 93 | printy("Predefined format [rI]This is red and with italic style@ also predefined format") 94 | ``` 95 | ![Printy inline red italic](.github/printy_inline_red_italic.png) 96 | 97 | The text that is not surrounded by the format syntax will remain with the predefined 98 | format. 99 | 100 | But you can always override this predefined format for inline format specifying the flags 101 | in the 'predefined' parameter 102 | ```python 103 | printy("Now this is blue [rI]Still red italic@ and also blue", predefined="b") 104 | ``` 105 | ![printy changing predefined](.github/printy_changing_predefined.png) 106 | 107 | Or, you can override the whole format without changing the inline format with a global flag: 108 | ```python 109 | printy("Now i am still blue, [rI]and also me@, and me as well ", "b") 110 | ``` 111 | ![Printy overriding inline with global](.github/printy_override_inline_with_global.png) 112 | 113 | You can combine it with f-strings: 114 | ```python 115 | a = 60 116 | printy(f"The day has [yB]{ 24 * a }@ minutes") 117 | ``` 118 | ![Printy f-strings](.github/printy_f_strings.png) 119 | 120 | Printy also supports reading from a file, just pass the path to your file 121 | in the file parameter: 122 | 123 | ```python 124 | # NOTE: Here, it is necessary to specify the flags (if you want) 125 | # in the 'flags' parameter 126 | printy(file="/path/to/your/file/file.extension", flags="cU") 127 | ``` 128 | ![Printy from file](.github/printy_from_file.png) 129 | 130 | You can also pretty print your dictionaries, lists, tuples, sets, and objects: 131 | 132 | ```python 133 | my_dict = {'id': 71, 'zip_codes': ['050001', '050005', '050011', '050015', '050024'], 'code': '05001', 'country': {'code': 'co'}, 'city_translations': [{'language_code': 'es', 'name': 'Medellín'}], 'flag': None} 134 | printy(my_dict) 135 | ``` 136 | 137 | ![Printy pretty dict](.github/printy_pretty_dict_four_indentation.png) 138 | 139 | ```python 140 | my_dict = {'id': 71, 'zip_codes': ['050001', '050005', '050011', '050015', '050024'], 'code': '05001', 'country': {'code': 'co'}, 'city_translations': [{'language_code': 'es', 'name': 'Medellín'}], 'flag': None} 141 | printy(my_dict, indentation=2) 142 | ``` 143 | 144 | ![Printy pretty dict](.github/printy_pretty_dict_two_indentation.png) 145 | 146 | ### New in v2.2.0 147 | 148 | #### Untrusted sources 149 | 150 | When dealing with untrusted sources, like, user input, we need to ensure the text is properly escaped 151 | before we pass it to printy. For that, we can use the funtion `escape` integrated with printy. 152 | 153 | Let's say we have and `email` variable that it's fill by an untrusted source: 154 | 155 | ```python 156 | from printy import printy, escape 157 | 158 | # Comes from an untrusted source 159 | email = 'example@example.com' 160 | 161 | # Without escaping it 162 | printy(f'This is your email: [nB]{email}@') 163 | 164 | # Escaping it 165 | printy(f'This is your email: [nB]{escape(email)}@') 166 | ``` 167 | ![Printy escape](.github/escape_printy.png) 168 | 169 | ### New in v2.2.0 170 | 171 | #### Background Colors 172 | 173 | Now, we can define the background color of the text, either on inline formats or with global flags, we simply pass the color flag between two brackets: 174 | 175 | ```python 176 | from printy import printy 177 | 178 | # Global format 179 | printy('Green bold text over a red background', 'nB{r}') 180 | 181 | # Inline format 182 | printy('Normal Text [nB{r}]Green bold text over a red background@ Also normal') 183 | ``` 184 | 185 | ![Printy background](.github/background_printy.png) 186 | 187 | ## What about input()? 188 | 189 | Printy also includes an alternative function for the builtin input(), that, not only 190 | lets us applies formats to the prompted message (if passed), but also, we can force 191 | the user to enter a certain type of data. 192 | ```python 193 | from printy import inputy 194 | ``` 195 | Let's say we want to get an integer from the user's input, for that, we can set 196 | type='int' in the 'inputy' function (we can specify formats the same way we'd do 197 | with printy) 198 | ```python 199 | fruits = ["Apple", "Orange", "Pineapple"] 200 | fruit = inputy("Select a fruit: ", options=fruits, condition="i") 201 | 202 | qty = inputy("How many [yBU]%ss@ do you want?" % fruit, predefined="rB", type="int", condition="+") 203 | 204 | confirmation = inputy("Are you sure you want [r]%d@ %ss?" % (qty, fruit), type="bool", options=["y", "n"], condition="i") 205 | ``` 206 | 207 | In all of the above examples, if the user enters a value with a type other than 208 | the one specified in 'type' (default is 'str'), the message will show again and will prompt also a warning 209 | (and so on until the user enters a valid value according to the type) 210 | 211 | You can pass certain conditions to validate the input, for example, you can 212 | pass ```condition="+"``` on an input with type 'int' to force the user to enter 213 | a positive integer (valid also for 'float'), check the complete options below 214 | 215 | **The best part** is that the returned value's type is also the one of the specified 216 | type, therefore, from the above examples, both *fruit* will be str, *qty* will be integer, and 217 | *confirmation* will be a boolean, so, you're gonna get the information right as you need it. 218 | 219 | ![Printy inputy Demo](.github/inputy_example.png) 220 | 221 | ### New in v2.1.0 222 | You can also add some restriction for numbers: max_digits and max_decimals 223 | 224 | ![Printy inputy max digits and max decimals](.github/inputy_max_digits_max_decimals.png) 225 | 226 | 227 | ## Curious? 228 | 229 | If you want to know what's behind the scenes, you can get the text with all the ANSI escaped sequences, 230 | for that, use the ```raw_format()``` function. 231 | 232 | ```python 233 | from printy import raw_format 234 | raw_text = raw_format("Some [rB]formatted@ [yIU]text@") 235 | print(repr(raw_text)) 236 | print(raw_text) 237 | ``` 238 | 239 | ![Printy raw format](.github/printy_raw_format.png) 240 | 241 | For convenience, we have stored all colors and formats flags in list, in case you need them: 242 | 243 | ```python 244 | from printy import COLORS, FORMATS 245 | print(COLORS) 246 | print(FORMATS) 247 | ``` 248 | 249 | ![Printy COLORS FORMATS](.github/printy_COLORS_FORMATS.png) 250 | 251 | ## API 252 | 253 | ### printy() 254 | 255 | | Parameters | type | | Description | 256 | | --- | --- | --- | --- | 257 | | value | str | required | Value to be formatted | 258 | | flags | str | optional | Global flags to be applied, they can be passed in the 'value' with the following syntax: [flags]value@ (check [List 1](#list-1-flags) for more info)| 259 | | predefined | str | optional | A set of flags to apply to the value as its predefined value | 260 | | file | str | optional | A path to a file where we want to read the value from | 261 | | end | str | optional | A value to be appended to the value, default is '\n' | 262 | | pretty | bool | optional | True if we want to pretty print objects, False if we do not (default True) | 263 | | indentation | int | optional | Indentation when pretty printing dictionaries or any iterable (default 4) | 264 | 265 | ### inputy() 266 | plus printy() parameters 267 | 268 | | Parameters | type | | Description | 269 | | --- | --- | --- | --- | 270 | | type | str | optional | Type of value we want the user to enter (check [List 2](#list-2-types) for more info)| 271 | | options | list | optional | Valid only for types 'str' and 'bool', a list of options to scope the value | 272 | | render_options | bool | optional | Specify whether we want to display the options to the user or not | 273 | | default | str | optional | If no value is entered, this one will be taken, make sure that it belongs to the options list (if passed) | 274 | | condition | str | optional | A character that applies certain restrictions to the value (check [List 3](#list-3-conditions) for mor info | 275 | | max_digits | int | optional | Adds a restriction for numbers about the maximum number of digits that it should have | 276 | | max_decimals | int | optional | Adds a restriction for numbers about the maximum number of decimals that it should have | 277 | 278 | ### List 1 'flags' 279 | 280 | **COLORS** 281 | - k - Applies a black color to the text 282 | - g - Applies a grey color to the text 283 | - w - Applies a white color to the text 284 | - - Applies a lightred color to the text 287 | - - Applies a lightgreen color to the text 290 | - - Applies a lightyellow color to the text 293 | - - Applies a lightblue color to the text 296 | - - Applies a lightmagenta color to the text 299 | - - Applies a lightcyan color to the text 302 | - - Applies a lightorange color to the text 305 | - \

- Applies a lightpurple color to the text 308 | 309 | **FORMATS** 310 | - B - Applies a bold font weight to the text 311 | - U - Applies an underline to the text 312 | - I - Applies an italic font type to the text 313 | - H - Highlights the text 314 | - S - Crosses out the text, aka Strike 315 | - D - Dim effect 316 | 317 | ### List 2 'types' 318 | - 'int': Value must be an integer or a string that can be turn into an integer, returns the value as an integer 319 | - 'float': Value must be a float or a string that can be turn into a float, returns the value as a float 320 | - 'bool': A string matching 'True' or 'False' if no options are passed, otherwise, a string that matches one of the options, returns the value as a boolean 321 | - 'str': The default type, if 'options' is passed, then the string must match one of the options or its item number. 322 | 323 | ### List 3 'conditions' 324 | - '+': Valid for 'int' and 'float' types only. The value must be a **positive** number 325 | - '-': Valid for 'int' and 'float' types only. The value must be a **negative** number 326 | - 'i': valid for 'str' and 'bool' types only. The value is case insensitive, by default it is case sensitive 327 | 328 | 329 | ## Changelog 330 | 331 | [Changelog.md](CHANGELOG.md) 332 | 333 | ## Dependencies 334 | 335 | Printy currently supports Python 3.5 and up. Printy is a cross-platform library 336 | 337 | ## Contributing 338 | 339 | Please feel free to contact me if you want to be part of the project and contribute. 340 | Fork or clone, push to your fork, make a pull request, let's make this a better app 341 | every day!! 342 | 343 | ## Contributors ✨ 344 | 345 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 |

Edgardo Obregón

💻 ⚠️ 💡 🤔 🚧 📖 🐛

farahduk

🤔 💻 🚧

Mihir Singh

⚠️ 💻

musicprogram

📓

Tanmay

🤔

Oliver Bentham

📓
360 | 361 | 362 | 363 | 364 | 365 | 366 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -------------------------------------------------------------------------------- /printy/core.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from .exceptions import InvalidInputType 4 | from .flags import Flags 5 | 6 | LINUX = 'Linux' 7 | WINDOWS = 'Windows' 8 | OSX = 'Darwin' 9 | 10 | # For format() and format_input() 11 | default_end = '\n' 12 | 13 | 14 | class Printy: 15 | """ 16 | Applies a format to the output of the print statement according 17 | to the flag (or flags). 18 | 19 | We can either set a global set of flags like >>> printy('Some text', 'rB') 20 | or set inline formats with the especial characters 21 | like >>> printy('[rB]Some@ [y]text@') 22 | """ 23 | 24 | # For inline formatting we'll use special characters to catch the flags 25 | end_format_char = '@' 26 | open_flag_char = '[' 27 | close_flag_char = ']' 28 | special_chars = [end_format_char, open_flag_char, close_flag_char] 29 | 30 | # Actions for inline formats 31 | START_FLAGS = 'start_flags' 32 | START_FORMAT = 'start_format' 33 | END_FORMAT = 'end_format' 34 | ESCAPE_CHAR = 'escape_char' 35 | 36 | def __init__(self): 37 | self.platform = platform.system() 38 | self.virtual_terminal_processing = self.set_windows_console_mode() 39 | 40 | def set_windows_console_mode(self): 41 | """ 42 | For Windows os to work and get the escape sequences correctly, 43 | we'll need to enable the variable ENABLE_VIRTUAL_TERMINAL_PROCESSING 44 | 45 | @Thanks to Mihir Singh (mihirs16) for this big improvement 46 | """ 47 | # In case there is some error while setting it up, we returns False to 48 | # indicate that windows will not print the escape sequences correctly, 49 | # so we can print out the cleaned text 50 | if self.platform == WINDOWS: 51 | try: 52 | from ctypes import windll 53 | k = windll.kernel32 54 | k.SetConsoleMode(k.GetStdHandle(-11), 7) 55 | return True 56 | except ImportError: 57 | return False 58 | return False 59 | 60 | @classmethod 61 | def _define_char(cls, prev: str, current: str) -> bool: 62 | """ 63 | Helper method that'll tell us if a character has to be treated as a 64 | special one or it is part of the text that 's intended to be printed 65 | out. 66 | 67 | Takes the previous character and the current character, in case it is 68 | one of the special characters defined in the class and is prepended by 69 | a '\' means that it has not to be treated as a special one 70 | 71 | Returns True if it's a special character 72 | """ 73 | if current in cls.special_chars: 74 | if prev != '\\': 75 | return True 76 | return False 77 | 78 | @classmethod 79 | def _check_special_char_position(cls, last_special: str, special: str) -> str: 80 | """ 81 | Returns an action to execute if the character is well placed. It should 82 | only be applied over special characters. 83 | 84 | If it's not well placed, the character will be included in the text 85 | """ 86 | if special == cls.open_flag_char: 87 | # In this case the 'last_special' must always be 88 | # the 'end_format_char' or None if it's the first appearing 89 | if last_special not in [cls.end_format_char, None]: 90 | return cls.ESCAPE_CHAR 91 | else: 92 | return cls.START_FLAGS 93 | elif special == cls.close_flag_char: 94 | # In this case the 'last_special' must always be the 'open_flag_char' 95 | if last_special != cls.open_flag_char: 96 | return cls.ESCAPE_CHAR 97 | else: 98 | return cls.START_FORMAT 99 | elif special == cls.end_format_char: 100 | # In this case the 'last_special' must always be the 'close_flag_char' 101 | # Or None if the text does not include any other formatting character 102 | if last_special in [cls.open_flag_char, cls.end_format_char, None]: 103 | return cls.ESCAPE_CHAR 104 | else: 105 | return cls.END_FORMAT 106 | 107 | @classmethod 108 | def _replace_escaped(cls, text: str) -> str: 109 | """ Replaces escaped special characters for the character itself """ 110 | for special_char in cls.special_chars: 111 | text = text.replace('\\' + special_char, special_char) 112 | return text 113 | 114 | @classmethod 115 | def _get_inline_format_as_tuple(cls, text: str) -> list: 116 | """ 117 | In case some inline formats have been applied we need to get a list of 118 | tuples indicating the formats to be applied via flags and the text 119 | where the format should be applied, for instance, if the text is: 120 | "[rB]Some@ Te[H]xt@" 121 | We'll get the list [('Some', 'rB), (' Te', None), ('xt', 'H')] 122 | """ 123 | prev = '' # Stores the last character in the loop 124 | last_special_char = None 125 | list_of_formats = [] # Final list to be returned 126 | 127 | # While looping the text, we'll get the formats to be applied to 128 | # certain section of that text 129 | section_text = [] 130 | section_flags = [] 131 | current_action = cls.START_FORMAT 132 | 133 | # Will tell us when we're at the last character of the loop 134 | counter = 0 135 | # Will tell us if we need to reset the sections variables 136 | close_section = False 137 | for char in text: 138 | is_special = cls._define_char(prev, char) 139 | if is_special: 140 | action = cls._check_special_char_position(last_special_char, 141 | char) 142 | 143 | if action == cls.ESCAPE_CHAR: 144 | # Add the character to the text 145 | if current_action == cls.START_FLAGS: 146 | section_flags.append(char) 147 | elif current_action == cls.START_FORMAT: 148 | section_text.append(char) 149 | else: 150 | # Here we know that the special character is well placed 151 | # and has a special meaning 152 | current_action = action 153 | 154 | if current_action == cls.END_FORMAT: 155 | current_action = cls.START_FORMAT 156 | close_section = True 157 | if last_special_char in [cls.end_format_char, None]: 158 | # Here we'll catch 'open_flag_char's 159 | current_action = cls.START_FLAGS 160 | close_section = True 161 | last_special_char = char 162 | else: 163 | if current_action == cls.START_FLAGS: 164 | section_flags.append(char) 165 | elif current_action == cls.START_FORMAT: 166 | section_text.append(char) 167 | prev = char 168 | counter += 1 169 | 170 | if counter == len(text) or close_section: 171 | # Reset the 'section_*' lists and add them (joined) 172 | # to the final list 173 | list_of_formats.append(( 174 | ''.join(section_text), 175 | ''.join(section_flags) if len(section_flags) > 0 else None 176 | )) 177 | section_text = [] 178 | section_flags = [] 179 | close_section = False 180 | 181 | return list_of_formats 182 | 183 | @classmethod 184 | def _get_cleaned_text(cls, text: str) -> str: 185 | """ Returns the cleaned value, with no formats """ 186 | tuple_text = cls._get_inline_format_as_tuple(text) 187 | return cls._replace_escaped(''.join(x[0] for x in tuple_text)) 188 | 189 | @classmethod 190 | def _escape_special_chars(cls, value): 191 | """ 192 | Escape all the special characters in the value (or the string 193 | representation of the value if an object is passed) 194 | """ 195 | _str = str(value) 196 | for char in cls.special_chars: 197 | _str = _str.replace(char, '\\' + char) 198 | return _str 199 | 200 | @classmethod 201 | def _pretty_print_object(cls, obj, indentation: int, level: int = 1) -> str: 202 | """ 203 | Recursive function to pretty print objects, with some 204 | indentations if needed. 205 | """ 206 | 207 | def _nested(nested_obj, nested_level: int = level) -> str: 208 | indentation_values = " " * indentation * nested_level 209 | indentation_braces = " " * indentation * (nested_level - 1) 210 | if isinstance(nested_obj, dict): 211 | return "{\n%(body)s%(indent_braces)s}" % { 212 | "body": "".join("%(indent_values)s[n>]\'%(key)s\'@: %(value)s[]\'" + cls._escape_special_chars(nested_obj) + "\'@" 246 | elif isinstance(nested_obj, bool) or nested_obj is None: 247 | return "[ str: 256 | """ 257 | In case a dictionary or a list or a set is passed as a value, we need 258 | to get the representation of it, and, escape all the '[', ']' and '@' 259 | characters that we find 260 | """ 261 | if isinstance(value, (dict, list, tuple, set)): 262 | if pretty: 263 | return cls._pretty_print_object(value, indentation) 264 | else: 265 | return cls._escape_special_chars(value) 266 | elif isinstance(value, bool) or value is None: 267 | return "[ str: 275 | """ 276 | Applies the format specified by the 'flags' to the 'value'. 277 | 278 | If 'flag's is passed, 'predefined' will be omitted. 279 | """ 280 | # In case an object is passed instead of a string 281 | value = self._repr_value(value, pretty, indentation) 282 | 283 | if self.platform == WINDOWS and not self.virtual_terminal_processing: 284 | text = self._get_cleaned_text(value) 285 | else: 286 | if flags: 287 | flags_values = Flags.get_flag_values(flags) 288 | value = self._get_cleaned_text(value) 289 | text = "%s%s%s" % ( 290 | Flags.join_flags(flags_values), 291 | value, 292 | Flags.get_end_of_line() 293 | ) 294 | else: 295 | tuple_text = self._get_inline_format_as_tuple(value) 296 | text = '' 297 | for section in tuple_text: 298 | section_text = self._replace_escaped(section[0]) 299 | section_flags = section[1] or predefined 300 | if section_flags: 301 | flags_values = Flags.get_flag_values(section_flags) 302 | text += "%s%s%s" % ( 303 | Flags.join_flags(flags_values), 304 | section_text, 305 | Flags.get_end_of_line() 306 | ) 307 | else: 308 | text += section_text 309 | return text 310 | 311 | @staticmethod 312 | def read_file(file: str) -> str: 313 | """ Given a file path, we read it and print it out """ 314 | file = str(file) 315 | with open(file) as f: 316 | text = f.read() 317 | return text 318 | 319 | def format(self, value='', flags='', predefined='', file='', 320 | end=default_end, pretty=True, indentation=4): 321 | """ Prints out the value """ 322 | value = self.read_file(file) if file else value 323 | print(self.get_formatted_text( 324 | value, flags, predefined, pretty, indentation), end=end 325 | ) 326 | 327 | def escape(self, value): 328 | """ 329 | Escape the special characters of the value passed to printy. Useful 330 | for untrusted sources. 331 | """ 332 | for char in self.special_chars: 333 | value = value.replace(char, '\{}'.format(char)) 334 | return value 335 | 336 | ##### ============= INPUTY ====================== 337 | 338 | # Types (str is the default) 339 | BOOL = 'bool' 340 | INT = 'int' 341 | FLOAT = 'float' 342 | STR = 'str' 343 | types = [BOOL, INT, FLOAT, STR] 344 | 345 | def check_boolean(self, value: str, options: dict, condition: str) -> tuple: 346 | """ 347 | Validates the value when the type must be a boolean, returns a boolean 348 | specifying whether it is a valid value, and if it is, returns the final 349 | value (after conversions if necessary) 350 | """ 351 | 352 | true_value, false_value = options["1"], options["2"] 353 | error_msg = "[o]%s@ is not a valid value, enter %s or %s" % ( 354 | value, 355 | true_value, 356 | false_value, 357 | ) 358 | if condition == 'i': 359 | true_value = true_value.lower() 360 | false_value = false_value.lower() 361 | value = value.lower() 362 | 363 | if value == true_value: 364 | return True, True 365 | elif value == false_value: 366 | return False, True 367 | else: 368 | self.format(error_msg) 369 | return False, False 370 | 371 | def _check_number(self, number_class, value: str = '', condition: str = '', 372 | max_digits: int = None, max_decimals: int = None) -> tuple: 373 | """ Validates the value when it is a number """ 374 | 375 | # the only options allowed are '+' and '-' 376 | error_msg = ( 377 | "\t[r>]Invalid Value:@ [o]%s@ is not a valid number" % value 378 | ) 379 | # Let's try to convert it to the number type 380 | valid_value = False 381 | try: 382 | value = number_class(value) 383 | except (ValueError, TypeError): 384 | if number_class == int: 385 | error_msg += ( 386 | ".\n\tPlease enter a [b>]rounded@ number, please check you are " 387 | "\n\tnot adding some [p>]decimal digits@" 388 | ) 389 | else: 390 | if condition: 391 | if condition == '+' and value >= 0: 392 | valid_value = True 393 | elif condition == '-' and value < 0: 394 | valid_value = True 395 | else: 396 | error_msg += ', \n\tMake sure it is a [y]%s@ number\n' % ( 397 | 'positive' if condition == '+' else 'negative' 398 | ) 399 | valid_value = False 400 | else: 401 | valid_value = True 402 | 403 | # Let's remove the negative sign if any 404 | str_value = str(value).replace('-', '') 405 | split_number = str_value.split('.') 406 | # Check max digits 407 | if max_digits is not None: 408 | if len(split_number[0]) > max_digits: 409 | error_msg += '. \n\tThe number must have [o]%d@ digits max' % max_digits 410 | valid_value = False 411 | if max_decimals is not None and len(split_number) > 1: 412 | if len(split_number[1]) > max_decimals: 413 | error_msg += '. \n\tThe number must have [o]%d@ decimals max' % max_decimals 414 | valid_value = False 415 | # Throw error 416 | if not valid_value: 417 | self.format(error_msg) 418 | 419 | return value, valid_value 420 | 421 | def check_integer(self, value: str, condition: str = '', max_digits: int = None) -> tuple: 422 | """ 423 | Validates the value when the type must be an integer, returns a boolean 424 | specifying whether it is a valid value, and if it is, returns the final 425 | value (after conversions if necessary) 426 | """ 427 | 428 | return self._check_number( 429 | int, 430 | value=value, 431 | condition=condition, 432 | max_digits=max_digits 433 | ) 434 | 435 | def check_float(self, value: str, condition: str = '', 436 | max_digits: int = None, max_decimals: int = None) -> tuple: 437 | """ 438 | Validates the value when the type must be an float, similar to integer check, 439 | but now it can have decimal digits, returns a boolean specifying whether it 440 | is a valid value, and if it is, returns the final 441 | value (after conversions if necessary) 442 | """ 443 | 444 | return self._check_number( 445 | float, 446 | value=value, 447 | condition=condition, 448 | max_digits=max_digits, 449 | max_decimals=max_decimals 450 | ) 451 | 452 | def check_string(self, value: str, options: dict = None, condition: str = ''): 453 | """ 454 | if options were passed, then it validates that the value belongs to 455 | those options 456 | """ 457 | error_msg = "[o]%s@ is not a valid value" % value 458 | if options: 459 | if condition == 'i': 460 | # then we need to create a dictionary where the keys are the 461 | # lowercase of the options' values, and the values are the 462 | # options' keys 463 | new_options = {} 464 | for key, val in options.items(): 465 | new_options[val.lower()] = key 466 | value = value.lower() 467 | 468 | if value in new_options.keys(): 469 | return options[new_options[value]], True 470 | elif value in options.keys(): 471 | return options[value], True 472 | else: 473 | self.format(error_msg) 474 | return value, False 475 | else: 476 | if value in options.values(): 477 | return value, True 478 | elif value in options.keys(): 479 | return options[value], True 480 | else: 481 | self.format(error_msg) 482 | return value, False 483 | else: 484 | return value, True 485 | 486 | @classmethod 487 | def _render_options(cls, options: dict, input_type: str, default: str, 488 | render_options: bool) -> str: 489 | """ 490 | Returns a string to present to the user specifying the available 491 | options. 'options' must be normalized. 492 | """ 493 | render = "" 494 | if render_options: 495 | if options and input_type in [cls.STR, cls.BOOL]: 496 | if input_type == cls.BOOL: 497 | if len(options) >= 2: 498 | render += " (%s/%s)" % (options['1'], options['2']) 499 | if default: 500 | # We escape the '[' and ']' so they can be formatted 501 | render += " \[%s\]" % default 502 | else: 503 | render += " default: %s\n" % default if default else '\n' 504 | for item, value in options.items(): 505 | render += " %s) %s\n" % (item, value) 506 | 507 | return render 508 | 509 | @classmethod 510 | def _normalize_options(cls, options: list, input_type: str): 511 | """ 512 | Takes the list passed as options and returns a dictionary enumerating 513 | all the options, i.e. if we pass ['option_1', 'options_2'], then we 514 | return {'1': 'option_1', '2': 'option_2'} 515 | """ 516 | normalized_options = {} 517 | if options is not None: 518 | if input_type == cls.BOOL: 519 | if len(options) < 2: 520 | options = ['True', 'False'] 521 | else: 522 | options = options[:2] 523 | else: 524 | if len(options) < 2: 525 | raise ValueError("'options' must contain at least two items") 526 | else: 527 | options = ['True', 'False'] if input_type == cls.BOOL else [] 528 | 529 | for option in range(len(options)): 530 | normalized_options[str(option + 1)] = str(options[option]) 531 | 532 | return normalized_options 533 | 534 | @staticmethod 535 | def _to_int(value): 536 | """ 537 | Helper function to convert a value into an integer, useful 538 | to validate some parameters 539 | """ 540 | if value is not None: 541 | try: 542 | value = int(value) 543 | except (ValueError, TypeError): 544 | raise 545 | 546 | return value 547 | 548 | def format_input(self, *args, **kwargs): 549 | """ 550 | Colorize the text prompted by input() and applies some validation. 551 | 552 | Also, it takes an additional parameter 'type', to tell the prompt not 553 | to accept a format other than the specified. As every input is converted 554 | to strings, a string that can be converted to the specified type is 555 | allowed. For example, if type=int, then the user would be forced to 556 | enter a number or a string that can be converted into an integer 557 | """ 558 | 559 | # If passed, we'll force the user to write a value with the specific 560 | # input_types' format. 561 | input_type = kwargs.get('type', self.STR) 562 | if input_type not in self.types: 563 | raise InvalidInputType(input_type) 564 | 565 | # A list containing the value valid values 566 | options = kwargs.get('options', None) 567 | # render_options defines whether the options should be rendered or not 568 | render_options = kwargs.get('render_options', True) 569 | # when no value is entered, the default will be added 570 | default = str(kwargs.get('default', '')) 571 | # Tells us the conditions according to the type, for numbers 572 | # we can say '+' or '-' whether we want only positives values or negative 573 | # values, for strings and boolean we can specify 'i' if we want to allow 574 | # user to type cas insensitive values 575 | condition = str(kwargs.get('condition', '')) 576 | # Defines the max number of digits that a number (int or float) can have 577 | max_digits = self._to_int(kwargs.get('max_digits', None)) 578 | # Defines the max number of decipal numbers that the value can have, 579 | # only valid for 'float' type 580 | max_decimals = self._to_int(kwargs.get('max_decimals', None)) 581 | 582 | # Include the value for the get_formatted_text function 583 | text, flags = '', '' 584 | if len(args) == 0: 585 | args = [text, flags] 586 | elif len(args) == 1: 587 | text = args[0] 588 | else: 589 | text, flags = args[0], args[1] 590 | 591 | # Normalize the options 592 | options = self._normalize_options(options, input_type) 593 | # Will tell us whether the user sent a value value according to the 594 | # specified type or not 595 | valid_value = False 596 | result = None 597 | while not valid_value: 598 | render_options = self._render_options(options, input_type, default, render_options) 599 | # Prints out the message if any was passed 600 | result = str( 601 | input( 602 | self.get_formatted_text(*args, **kwargs) 603 | + self.get_formatted_text(render_options, flags) 604 | ) 605 | ) 606 | 607 | if result == '': 608 | result = default 609 | 610 | if input_type == self.BOOL: 611 | result, valid_value = self.check_boolean(result, options, condition) 612 | 613 | elif input_type == self.INT: 614 | result, valid_value = self.check_integer(result, condition, max_digits) 615 | 616 | elif input_type == self.FLOAT: 617 | result, valid_value = self.check_float(result, condition, max_digits, max_decimals) 618 | 619 | else: 620 | result, valid_value = self.check_string(result, options, condition) 621 | 622 | return result 623 | -------------------------------------------------------------------------------- /tests/tests_core.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from printy.exceptions import InvalidFlag, InvalidInputType 4 | from printy.core import Printy, WINDOWS 5 | from printy.flags import Flags 6 | 7 | 8 | class TestGlobalFlagsPrinty(unittest.TestCase): 9 | """ Test case for formatting with a global set of flags specified """ 10 | 11 | def setUp(self): 12 | self.sample_text = "Some Text To Print Out" 13 | self.printy = Printy() 14 | self.raw_text = self.printy.get_formatted_text 15 | self.esc = self.printy.escape 16 | 17 | def test_empty_value(self): 18 | """ Tests passing an empty value print's nothing""" 19 | text = '' 20 | result = self.raw_text(text) 21 | 22 | self.assertEqual(result, text) 23 | 24 | def test_empty_value_with_flags(self): 25 | """ 26 | Tests that passing and empty value with some flags returns the 27 | escape ansi characters 28 | """ 29 | text = '' 30 | flags = 'rBH' 31 | result = self.raw_text(text, flags) 32 | expected_result = "%s%s" % ( 33 | Flags.join_flags(Flags.get_flag_values(flags)), 34 | Flags.get_end_of_line() 35 | ) 36 | 37 | self.assertEqual(result, expected_result) 38 | 39 | def test_single_invalid_flag(self): 40 | """ 41 | Tests that passing an invalid flag (only one) 42 | raises and exception 43 | """ 44 | invalid_flag = 'P' 45 | with self.assertRaises(InvalidFlag): 46 | self.printy.format(self.sample_text, invalid_flag) 47 | 48 | def test_multiple_invalid_flag(self): 49 | """ 50 | Tests that passing multiple invalid flags raises an 51 | exception with the first invalid flag found 52 | """ 53 | # P and G are invalid, should raise InvalidFlag 54 | # with 'P' as invalid flag 55 | flags = 'yBPGr' 56 | with self.assertRaises(InvalidFlag) as e: 57 | self.printy.format(self.sample_text, flags) 58 | self.assertEqual(e.exception.flag, 'P') 59 | 60 | def test_high_intensity_flag_color(self): 61 | """ 62 | Checks the correct format is returned for a high 63 | intensity (>) flag color 64 | """ 65 | flag = 'p>' 66 | text = 'Hello' 67 | expected_text = '\x1b[38;5;98mHello\x1b[0m' 68 | 69 | self.assertEqual(self.raw_text(text, flag), expected_text) 70 | 71 | def test_low_intensity_flag_color(self): 72 | """ 73 | Checks the correct format is returned for a low 74 | intensity (<) flag color 75 | """ 76 | flag = ']\'John Doe\'@[]\'age\'@: [c]34@[' 329 | pretty_builtin = Printy._repr_value(builtin_obj) 330 | 331 | class Person: 332 | def __str__(self): 333 | return '[c]I am a person@' 334 | custom_str = Person() 335 | # Notice how it should not return the escaped character 336 | expected_custom_result = '[c]I am a person@' 337 | pretty_custom = Printy._repr_value(custom_str) 338 | 339 | self.assertEqual(expected_builtin_result, pretty_builtin) 340 | self.assertEqual(expected_custom_result, pretty_custom) 341 | 342 | def test_pretty_object_in_dictionary(self): 343 | """ 344 | Test pretty printing an str method of an object inside a dictionary 345 | or any iterable, it should give it a light magenta color 346 | """ 347 | dict_to_print = {'class': int} 348 | expected_result = '{\n [n>]\'class\'@: [