├── .travis.yml ├── LICENSE ├── README.md ├── codecov.yml ├── fancycompleter.py ├── misc ├── fancycompleterrc.py ├── libreadline.so.6-32bit ├── libreadline.so.6-64bit ├── readline-escape-5.2.patch └── readline-escape-6.0.patch ├── screenshot.png ├── setup.py ├── testing ├── __init__.py ├── conftest.py ├── test_configurableclass.py └── test_fancycompleter.py └── tox.ini /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | 4 | env: 5 | global: 6 | - PYTEST_ADDOPTS="-vv --cov --cov-report=xml" 7 | 8 | stages: 9 | - name: test 10 | if: tag IS NOT present 11 | - name: release 12 | if: tag IS present 13 | 14 | jobs: 15 | include: 16 | - os: windows 17 | language: shell 18 | env: 19 | - PATH=/c/Python38:/c/Python38/Scripts:$PATH 20 | - TOXENV=py38-coverage 21 | before_install: 22 | - choco install --no-progress python 23 | 24 | - os: osx 25 | osx_image: xcode10.2 26 | language: generic 27 | env: TOXENV=py37-coverage 28 | before_install: 29 | - ln -sfn "$(which python3)" /usr/local/bin/python 30 | - python -V 31 | - test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37 32 | 33 | - python: '2.7' 34 | env: TOXENV=py27-coverage 35 | - python: '3.4' 36 | env: TOXENV=py34-coverage 37 | - python: '3.5' 38 | env: TOXENV=py35-coverage 39 | - python: '3.6' 40 | env: TOXENV=py36-coverage 41 | - python: '3.7' 42 | env: TOXENV=py37-coverage 43 | - python: '3.8' 44 | env: TOXENV=py38-coverage 45 | - python: 'pypy' 46 | env: TOXENV=pypy-coverage 47 | - python: 'pypy3' 48 | env: TOXENV=pypy3-coverage 49 | 50 | - python: '3.7' 51 | env: TOXENV=checkqa 52 | 53 | - stage: release 54 | script: skip 55 | install: skip 56 | after_success: true 57 | env: 58 | deploy: 59 | provider: pypi 60 | distributions: sdist bdist_wheel 61 | user: __token__ 62 | password: 63 | secure: "GCyMei2qFzd9AN0EmT9AGqO0zQFtab8Yff4O9zmpDn34hk7TRhQdAHqPXTj0GovYjN783y31jQdVPbEsFiXUAtEu6rfOBwTtVvNCHGVdDQ0nhZFZVwYD3NfhaV1UCq/ahs5AdUEARAPbR8lviH4PMByrMs3x+ul+bHfZ70QlD1xvC/wlkZ+C/FWc5WiKbkqM5W/CUJoOnX7C5Cx/cI/VZI8X3N77t1J7fW4CEvk3nvU9CW8gDCcuJhq4Hr4oW85PsSCcJagwo1im3WSK+5rNTFlihoE1kGYtrDlWFrNFruAwobk9LSjk+GKTZqD6PFxilON/hiKavxHNYEBwwnfvpDTK87lQHU1LuLOjNMDn8pPOj8uvvKrx9y2BgtFcJzEq9oudtJOKYcxoVpLm/tmqB4QzlTWpOKXk769Sk7lZM9n+psu6wtAd1X8GH5qFon5z0YnNmaNFew5bKs3R/L3Eav1OyskA0zi4f/h8s98apnY4AGX7ul/xxoJhp3OXiSN75fMI6SUiNZLFgRUmFNqJ6pzCqHDbV0y60EeH+5BBLIdKc/D+YsuqDZYAjkN4ze6JVzGtxSSK9tuZyKJJ7zPXT1qdZxXRF0XOHRcVTxl+tNBncCmVvrJmf8QvQ6FteShZqTu3qfWWAubCOGVrxr0aVVZkYR6izNrAsp+J2/ETs5Q=" 64 | on: 65 | tags: true 66 | repo: pdbpp/fancycompleter 67 | 68 | install: 69 | - pip install tox==3.12.1 70 | # NOTE: need to upgrade virtualenv to allow "Direct url requirement" with 71 | # installation in tox. 72 | - pip install virtualenv==16.6.0 73 | 74 | script: 75 | - tox 76 | 77 | after_script: 78 | - | 79 | if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then 80 | curl --version 81 | curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash > codecov.sh 82 | bash codecov.sh -Z -X fix -f coverage.xml -n $TOXENV -F "${TRAVIS_OS_NAME}" 83 | fi 84 | 85 | # Only master and releases. PRs are used otherwise. 86 | branches: 87 | only: 88 | - master 89 | - /^\d+\.\d+(\.\d+)?(-\S*)?$/ 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2008, Antonio Cuni and others 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fancycompleter: colorful Python TAB completion 2 | ============================================== 3 | 4 | What is is? 5 | ----------- 6 | 7 | `fancycompleter` is a module to improve your experience in Python by 8 | adding TAB completion to the interactive prompt. It is an extension of 9 | the stdlib\'s 10 | [rlcompleter](http://docs.python.org/library/rlcompleter.html) module. 11 | 12 | Its best feature is that the completions are displayed in different 13 | colors, depending on their type: 14 | 15 | ![image](http://bitbucket.org/antocuni/fancycompleter/raw/5bf506e05ce7/screenshot.png) 16 | 17 | In the image above, strings are shown in green, functions in blue, 18 | integers and boolean in yellows, `None` in gray, types and classes in 19 | fuchsia. Everything else is plain white. 20 | 21 | `fancycompleter` is compatible with Python 3. However, by default colors 22 | don\'t work on Python 3, see the section [How do I get 23 | colors?](#how-do-i-get-colors) for details. 24 | 25 | Other features 26 | -------------- 27 | 28 | - To save space on screen, `fancycompleter` only shows the characters 29 | "after the dot". By contrast, in the example above `rlcompleter` 30 | shows everything prepended by `"sys."`. 31 | - If we press `` at the beginning of the line, a real tab 32 | character is inserted, instead of trying to complete. This is useful 33 | when typing function bodies or multi-line statements at the prompt. 34 | - Unlike `rlcompleter`, `fancycompleter` **does** complete expressions 35 | containing dictionary or list indexing. For example, 36 | `mydict['foo'].` works (assuming that `mydict` is a dictionary 37 | and that it contains the key `'foo'`, of course :-)). 38 | - Starting from Python 2.6, if the completed name is a callable, 39 | `rlcompleter` automatically adds an open parenthesis `(`. This is 40 | annoying in case we do not want to really call it, so 41 | `fancycompleter` disable this behaviour. 42 | 43 | Installation 44 | ------------ 45 | 46 | First, install the module with `pip` or `easy_install`: 47 | 48 | $ pip install fancycompleter 49 | 50 | Then, at the Python interactive prompt: 51 | 52 | >>> import fancycompleter 53 | >>> fancycompleter.interact(persist_history=True) 54 | >>> 55 | 56 | If you want to enable `fancycompleter` automatically at startup, you can 57 | add those two lines at the end of your 58 | [PYTHONSTARTUP](http://docs.python.org/using/cmdline.html#envvar-PYTHONSTARTUP) 59 | script. 60 | 61 | If you do **not** have a `PYTHONSTARTUP` script, the 62 | following command will create one for you in `~/python_startup.py`: 63 | 64 | $ python -m fancycompleter install 65 | 66 | On Windows, `install` automatically sets the `PYTHONSTARTUP` environment 67 | variable. On other systems, you need to add the proper command in 68 | `~/.bashrc` or equivalent. 69 | 70 | **Note**: depending on your particular system, `interact` might need to 71 | play dirty tricks in order to display colors, although everything should 72 | "just work". In particular, the call to `interact` should be the last 73 | line in the startup file, else the next lines might not be executed. See 74 | section [What is really going on?](#what-is-really-going-on) for 75 | details. 76 | 77 | How do I get colors? 78 | -------------------- 79 | 80 | If you are using **PyPy**, you can stop reading now, as `fancycompleter` 81 | will work out of the box. 82 | 83 | If you are using **CPython on Linux/OSX** and you installed 84 | `fancycompleter` with `pip` or `easy_install`, they automatically 85 | installed `pyrepl` as a requirement, and you should also get colors out 86 | of the box. If for some reason you don\'t want to use `pyrepl`, you 87 | should keep on reading. 88 | 89 | By default, in CPython line input and TAB completion are handled by [GNU 90 | readline](http://tiswww.case.edu/php/chet/readline/rltop.html) (at least 91 | on Linux). However, `readline` explicitly strips escape sequences from 92 | the completions, so completions with colors are not displayed correctly. 93 | 94 | There are two ways to solve it: 95 | 96 | > - (suggested) don\'t use `readline` at all and rely on 97 | > [pyrepl](http://codespeak.net/pyrepl/) 98 | > - use a patched version of `readline` to allow colors 99 | 100 | By default, `fancycompleter` tries to use `pyrepl` if it finds it. To 101 | get colors you need a recent version, \>= 0.8.2. 102 | 103 | Starting from version 0.6.1, `fancycompleter` works also on **Windows**, 104 | relying on [pyreadline](https://pypi.python.org/pypi/pyreadline). At the 105 | moment of writing, the latest version of `pyreadline` is 2.1, which does 106 | **not** support colored completions; here is the [pull 107 | request](https://github.com/pyreadline/pyreadline/pull/48) which adds 108 | support for them. To enable colors, you can install `pyreadline` from 109 | [this fork](https://github.com/antocuni/pyreadline) using the following 110 | command: 111 | 112 | pip install --upgrade https://github.com/antocuni/pyreadline/tarball/master 113 | 114 | If you are using **Python 3**, `pyrepl` does not work, and thus is not 115 | installed. Your only option to get colors is to use a patched 116 | `readline`, as explained below. 117 | 118 | I really want to use readline 119 | ----------------------------- 120 | 121 | This method is not really recommended, but if you really want, you can 122 | use use a patched readline: you can find the patches in the `misc/` 123 | directory: 124 | 125 | > - for 126 | > [readline-5.2](http://bitbucket.org/antocuni/fancycompleter/src/tip/misc/readline-escape-5.2.patch) 127 | > - for 128 | > [readline-6.0](http://bitbucket.org/antocuni/fancycompleter/src/tip/misc/readline-escape-6.0.patch) 129 | 130 | You can also try one of the following precompiled versions, which has 131 | been tested on Ubuntu 10.10: remember to put them in a place where the 132 | linker can find them, e.g. by setting `LD_LIBRARY_PATH`: 133 | 134 | > - readline-6.0 for 135 | > [32-bit](http://bitbucket.org/antocuni/fancycompleter/src/tip/misc/libreadline.so.6-32bit) 136 | > - readline-6.0 for 137 | > [64-bit](http://bitbucket.org/antocuni/fancycompleter/src/tip/misc/libreadline.so.6-64bit) 138 | 139 | Once it is installed, you should double-check that you can find it, e.g. 140 | by running `ldd` on Python\'s `readline.so` module: 141 | 142 | $ ldd /usr/lib/python2.6/lib-dynload/readline.so | grep readline 143 | libreadline.so.6 => /home/antocuni/local/32/lib/libreadline.so.6 (0x00ee7000) 144 | 145 | Finally, you need to force `fancycompleter` to use colors, since by 146 | default, it uses colors only with `pyrepl`: you can do it by placing a 147 | custom config file in `~/.fancycompleterrc.py`. An example config file 148 | is 149 | [here](http://bitbucket.org/antocuni/fancycompleter/src/tip/misc/fancycompleterrc.py) 150 | (remind that you need to put a dot in front of the filename!). 151 | 152 | Customization 153 | ------------- 154 | 155 | To customize the configuration of fancycompleter, you need to put a file 156 | named `.fancycompleterrc.py` in your home directory. The file must 157 | contain a class named `Config` inheriting from `DefaultConfig` and 158 | overridding the desired values. 159 | 160 | What is really going on? 161 | ------------------------ 162 | 163 | The default and preferred way to get colors is to use `pyrepl`. However, 164 | there is no way to tell CPython to use `pyrepl` instead of the built-in 165 | readline at the interactive prompt: this means that even if we install 166 | our completer inside pyrepl\'s readline library, the interactive prompt 167 | won\'t see it. 168 | 169 | The issue is simply solved by avoiding to use the built-in prompt: 170 | instead, we use a pure Python replacement based on 171 | [code.InteractiveConsole](http://docs.python.org/library/code.html#code.InteractiveConsole). 172 | This brings us also some niceties, such as the ability to do multi-line 173 | editing of the history. 174 | 175 | The console is automatically run by `fancycompleter.interact()`, 176 | followed by `sys.exit()`: this way, if we execute it from the script in 177 | `PYTHONSTARTUP`, the interpreter exits as soon as we finish the use the 178 | prompt (e.g. by pressing CTRL-D, or by calling `quit()`). This way, we 179 | avoid to enter the built-in prompt and we get a behaviour which closely 180 | resembles the default one. This is why in this configuration lines after 181 | `fancycompleter.interact()` might not be run. 182 | 183 | Note that if we are using `readline` instead of `pyrepl`, the trick is 184 | not needed and thus `interact()` will simply returns, letting the 185 | built-in prompt to show up. The same is true if we are running PyPy, as 186 | its built-in prompt is based on pyrepl anyway. 187 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: true 4 | patch: true 5 | changes: true 6 | comment: false 7 | -------------------------------------------------------------------------------- /fancycompleter.py: -------------------------------------------------------------------------------- 1 | """ 2 | fancycompleter: colorful TAB completion for Python prompt 3 | """ 4 | from __future__ import with_statement 5 | from __future__ import print_function 6 | 7 | import rlcompleter 8 | import sys 9 | import types 10 | import os.path 11 | from itertools import count 12 | 13 | PY3K = sys.version_info[0] >= 3 14 | 15 | # python3 compatibility 16 | # --------------------- 17 | try: 18 | from itertools import izip 19 | except ImportError: 20 | izip = zip 21 | 22 | try: 23 | from types import ClassType 24 | except ImportError: 25 | ClassType = type 26 | 27 | try: 28 | unicode 29 | except NameError: 30 | unicode = str 31 | 32 | # ---------------------- 33 | 34 | 35 | class LazyVersion(object): 36 | 37 | def __init__(self, pkg): 38 | self.pkg = pkg 39 | self.__version = None 40 | 41 | @property 42 | def version(self): 43 | if self.__version is None: 44 | self.__version = self._load_version() 45 | return self.__version 46 | 47 | def _load_version(self): 48 | try: 49 | from pkg_resources import get_distribution, DistributionNotFound 50 | except ImportError: 51 | return 'N/A' 52 | # 53 | try: 54 | return get_distribution(self.pkg).version 55 | except DistributionNotFound: 56 | # package is not installed 57 | return 'N/A' 58 | 59 | def __repr__(self): 60 | return self.version 61 | 62 | def __eq__(self, other): 63 | return self.version == other 64 | 65 | def __ne__(self, other): 66 | return not self == other 67 | 68 | 69 | __version__ = LazyVersion(__name__) 70 | 71 | # ---------------------- 72 | 73 | 74 | class Color: 75 | black = '30' 76 | darkred = '31' 77 | darkgreen = '32' 78 | brown = '33' 79 | darkblue = '34' 80 | purple = '35' 81 | teal = '36' 82 | lightgray = '37' 83 | darkgray = '30;01' 84 | red = '31;01' 85 | green = '32;01' 86 | yellow = '33;01' 87 | blue = '34;01' 88 | fuchsia = '35;01' 89 | turquoise = '36;01' 90 | white = '37;01' 91 | 92 | @classmethod 93 | def set(cls, color, string): 94 | try: 95 | color = getattr(cls, color) 96 | except AttributeError: 97 | pass 98 | return '\x1b[%sm%s\x1b[00m' % (color, string) 99 | 100 | 101 | class DefaultConfig: 102 | 103 | consider_getitems = True 104 | prefer_pyrepl = True 105 | use_colors = 'auto' 106 | readline = None # set by setup() 107 | using_pyrepl = False # overwritten by find_pyrepl 108 | 109 | color_by_type = { 110 | types.BuiltinMethodType: Color.turquoise, 111 | types.MethodType: Color.turquoise, 112 | type((42).__add__): Color.turquoise, 113 | type(int.__add__): Color.turquoise, 114 | type(str.replace): Color.turquoise, 115 | 116 | types.FunctionType: Color.blue, 117 | types.BuiltinFunctionType: Color.blue, 118 | 119 | ClassType: Color.fuchsia, 120 | type: Color.fuchsia, 121 | 122 | types.ModuleType: Color.teal, 123 | type(None): Color.lightgray, 124 | str: Color.green, 125 | unicode: Color.green, 126 | int: Color.yellow, 127 | float: Color.yellow, 128 | complex: Color.yellow, 129 | bool: Color.yellow, 130 | } 131 | # Fallback to look up colors by `isinstance` when not matched 132 | # via color_by_type. 133 | color_by_baseclass = [ 134 | ((BaseException,), Color.red), 135 | ] 136 | 137 | def find_pyrepl(self): 138 | try: 139 | import pyrepl.readline 140 | import pyrepl.completing_reader 141 | except ImportError: 142 | return None 143 | self.using_pyrepl = True 144 | if hasattr(pyrepl.completing_reader, 'stripcolor'): 145 | # modern version of pyrepl 146 | return pyrepl.readline, True 147 | else: 148 | return pyrepl.readline, False 149 | 150 | def find_pyreadline(self): 151 | try: 152 | import readline 153 | import pyreadline # noqa: F401 # XXX: needed really? 154 | from pyreadline.modes import basemode 155 | except ImportError: 156 | return None 157 | if hasattr(basemode, 'stripcolor'): 158 | # modern version of pyreadline; see: 159 | # https://github.com/pyreadline/pyreadline/pull/48 160 | return readline, True 161 | else: 162 | return readline, False 163 | 164 | def find_best_readline(self): 165 | if self.prefer_pyrepl: 166 | result = self.find_pyrepl() 167 | if result: 168 | return result 169 | if sys.platform == 'win32': 170 | result = self.find_pyreadline() 171 | if result: 172 | return result 173 | import readline 174 | return readline, False # by default readline does not support colors 175 | 176 | def setup(self): 177 | self.readline, supports_color = self.find_best_readline() 178 | if self.use_colors == 'auto': 179 | self.use_colors = supports_color 180 | 181 | 182 | def my_execfile(filename, mydict): 183 | with open(filename) as f: 184 | code = compile(f.read(), filename, 'exec') 185 | exec(code, mydict) 186 | 187 | 188 | class ConfigurableClass: 189 | DefaultConfig = None 190 | config_filename = None 191 | 192 | def get_config(self, Config): 193 | if Config is not None: 194 | return Config() 195 | # try to load config from the ~/filename file 196 | filename = '~/' + self.config_filename 197 | rcfile = os.path.normpath(os.path.expanduser(filename)) 198 | if not os.path.exists(rcfile): 199 | return self.DefaultConfig() 200 | 201 | mydict = {} 202 | try: 203 | my_execfile(rcfile, mydict) 204 | except Exception as exc: 205 | import traceback 206 | 207 | sys.stderr.write("** error when importing %s: %r **\n" % (filename, exc)) 208 | traceback.print_tb(sys.exc_info()[2]) 209 | return self.DefaultConfig() 210 | 211 | try: 212 | Config = mydict["Config"] 213 | except KeyError: 214 | return self.DefaultConfig() 215 | 216 | try: 217 | return Config() 218 | except Exception as exc: 219 | err = "error when setting up Config from %s: %s" % (filename, exc) 220 | tb = sys.exc_info()[2] 221 | if tb and tb.tb_next: 222 | tb = tb.tb_next 223 | err_fname = tb.tb_frame.f_code.co_filename 224 | err_lnum = tb.tb_lineno 225 | err += " (%s:%d)" % (err_fname, err_lnum,) 226 | sys.stderr.write("** %s **\n" % err) 227 | return self.DefaultConfig() 228 | 229 | 230 | class Completer(rlcompleter.Completer, ConfigurableClass): 231 | """ 232 | When doing someting like a.b., display only the attributes of 233 | b instead of the full a.b.attr string. 234 | 235 | Optionally, display the various completions in different colors 236 | depending on the type. 237 | """ 238 | 239 | DefaultConfig = DefaultConfig 240 | config_filename = '.fancycompleterrc.py' 241 | 242 | def __init__(self, namespace=None, Config=None): 243 | rlcompleter.Completer.__init__(self, namespace) 244 | self.config = self.get_config(Config) 245 | self.config.setup() 246 | readline = self.config.readline 247 | if hasattr(readline, '_setup'): 248 | # this is needed to offer pyrepl a better chance to patch 249 | # raw_input. Usually, it does at import time, but is we are under 250 | # pytest with output captured, at import time we don't have a 251 | # terminal and thus the raw_input hook is not installed 252 | readline._setup() 253 | if self.config.use_colors: 254 | readline.parse_and_bind('set dont-escape-ctrl-chars on') 255 | if self.config.consider_getitems: 256 | delims = readline.get_completer_delims() 257 | delims = delims.replace('[', '') 258 | delims = delims.replace(']', '') 259 | readline.set_completer_delims(delims) 260 | 261 | def complete(self, text, state): 262 | """ 263 | stolen from: 264 | http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496812 265 | """ 266 | if text == "": 267 | return ['\t', None][state] 268 | else: 269 | return rlcompleter.Completer.complete(self, text, state) 270 | 271 | def _callable_postfix(self, val, word): 272 | # disable automatic insertion of '(' for global callables: 273 | # this method exists only in Python 2.6+ 274 | return word 275 | 276 | def global_matches(self, text): 277 | import keyword 278 | names = rlcompleter.Completer.global_matches(self, text) 279 | prefix = commonprefix(names) 280 | if prefix and prefix != text: 281 | return [prefix] 282 | 283 | names.sort() 284 | values = [] 285 | for name in names: 286 | clean_name = name.rstrip(': ') 287 | if clean_name in keyword.kwlist: 288 | values.append(None) 289 | else: 290 | try: 291 | values.append(eval(name, self.namespace)) 292 | except Exception as exc: 293 | values.append(exc) 294 | if self.config.use_colors and names: 295 | return self.color_matches(names, values) 296 | return names 297 | 298 | def attr_matches(self, text): 299 | expr, attr = text.rsplit('.', 1) 300 | if '(' in expr or ')' in expr: # don't call functions 301 | return [] 302 | try: 303 | thisobject = eval(expr, self.namespace) 304 | except Exception: 305 | return [] 306 | 307 | # get the content of the object, except __builtins__ 308 | words = set(dir(thisobject)) 309 | words.discard("__builtins__") 310 | 311 | if hasattr(thisobject, '__class__'): 312 | words.add('__class__') 313 | words.update(rlcompleter.get_class_members(thisobject.__class__)) 314 | names = [] 315 | values = [] 316 | n = len(attr) 317 | if attr == '': 318 | noprefix = '_' 319 | elif attr == '_': 320 | noprefix = '__' 321 | else: 322 | noprefix = None 323 | words = sorted(words) 324 | while True: 325 | for word in words: 326 | if (word[:n] == attr and 327 | not (noprefix and word[:n+1] == noprefix)): 328 | try: 329 | val = getattr(thisobject, word) 330 | except Exception: 331 | val = None # Include even if attribute not set 332 | 333 | if not PY3K and isinstance(word, unicode): 334 | # this is needed because pyrepl doesn't like unicode 335 | # completions: as soon as it finds something which is not str, 336 | # it stops. 337 | word = word.encode('utf-8') 338 | 339 | names.append(word) 340 | values.append(val) 341 | if names or not noprefix: 342 | break 343 | if noprefix == '_': 344 | noprefix = '__' 345 | else: 346 | noprefix = None 347 | 348 | if not names: 349 | return [] 350 | 351 | if len(names) == 1: 352 | return ['%s.%s' % (expr, names[0])] # only option, no coloring. 353 | 354 | prefix = commonprefix(names) 355 | if prefix and prefix != attr: 356 | return ['%s.%s' % (expr, prefix)] # autocomplete prefix 357 | 358 | if self.config.use_colors: 359 | return self.color_matches(names, values) 360 | 361 | if prefix: 362 | names += [' '] 363 | return names 364 | 365 | def color_matches(self, names, values): 366 | matches = [self.color_for_obj(i, name, obj) 367 | for i, name, obj 368 | in izip(count(), names, values)] 369 | # We add a space at the end to prevent the automatic completion of the 370 | # common prefix, which is the ANSI ESCAPE sequence. 371 | return matches + [' '] 372 | 373 | def color_for_obj(self, i, name, value): 374 | t = type(value) 375 | color = self.config.color_by_type.get(t, None) 376 | if color is None: 377 | for x, _color in self.config.color_by_baseclass: 378 | if isinstance(value, x): 379 | color = _color 380 | break 381 | else: 382 | color = '00' 383 | # hack: prepend an (increasing) fake escape sequence, 384 | # so that readline can sort the matches correctly. 385 | return '\x1b[%03d;00m' % i + Color.set(color, name) 386 | 387 | 388 | def commonprefix(names, base=''): 389 | """ return the common prefix of all 'names' starting with 'base' 390 | """ 391 | if base: 392 | names = [x for x in names if x.startswith(base)] 393 | if not names: 394 | return '' 395 | s1 = min(names) 396 | s2 = max(names) 397 | for i, c in enumerate(s1): 398 | if c != s2[i]: 399 | return s1[:i] 400 | return s1 401 | 402 | 403 | def has_leopard_libedit(config): 404 | # Detect if we are using Leopard's libedit. 405 | # Adapted from IPython's rlineimpl.py. 406 | if config.using_pyrepl or sys.platform != 'darwin': 407 | return False 408 | 409 | # Official Python docs state that 'libedit' is in the docstring for 410 | # libedit readline. 411 | return config.readline.__doc__ and 'libedit' in config.readline.__doc__ 412 | 413 | 414 | def setup(): 415 | """ 416 | Install fancycompleter as the default completer for readline. 417 | """ 418 | completer = Completer() 419 | readline = completer.config.readline 420 | if has_leopard_libedit(completer.config): 421 | readline.parse_and_bind("bind ^I rl_complete") 422 | else: 423 | readline.parse_and_bind('tab: complete') 424 | readline.set_completer(completer.complete) 425 | return completer 426 | 427 | 428 | def interact_pyrepl(): 429 | import sys 430 | from pyrepl import readline 431 | from pyrepl.simple_interact import run_multiline_interactive_console 432 | sys.modules['readline'] = readline 433 | run_multiline_interactive_console() 434 | 435 | 436 | def setup_history(completer, persist_history): 437 | import atexit 438 | readline = completer.config.readline 439 | # 440 | if isinstance(persist_history, (str, unicode)): 441 | filename = persist_history 442 | else: 443 | filename = '~/.history.py' 444 | filename = os.path.expanduser(filename) 445 | if os.path.isfile(filename): 446 | readline.read_history_file(filename) 447 | 448 | def save_history(): 449 | readline.write_history_file(filename) 450 | atexit.register(save_history) 451 | 452 | 453 | def interact(persist_history=None): 454 | """ 455 | Main entry point for fancycompleter: run an interactive Python session 456 | after installing fancycompleter. 457 | 458 | This function is supposed to be called at the end of PYTHONSTARTUP: 459 | 460 | - if we are using pyrepl: install fancycompleter, run pyrepl multiline 461 | prompt, and sys.exit(). The standard python prompt will never be 462 | reached 463 | 464 | - if we are not using pyrepl: install fancycompleter and return. The 465 | execution will continue as normal, and the standard python prompt will 466 | be displayed. 467 | 468 | This is necessary because there is no way to tell the standard python 469 | prompt to use the readline provided by pyrepl instead of the builtin one. 470 | 471 | By default, pyrepl is preferred and automatically used if found. 472 | """ 473 | import sys 474 | completer = setup() 475 | if persist_history: 476 | setup_history(completer, persist_history) 477 | if completer.config.using_pyrepl and '__pypy__' not in sys.builtin_module_names: 478 | # if we are on PyPy, we don't need to run a "fake" interpeter, as the 479 | # standard one is fake enough :-) 480 | interact_pyrepl() 481 | sys.exit() 482 | 483 | 484 | class Installer(object): 485 | """ 486 | Helper to install fancycompleter in PYTHONSTARTUP 487 | """ 488 | 489 | def __init__(self, basepath, force): 490 | fname = os.path.join(basepath, 'python_startup.py') 491 | self.filename = os.path.expanduser(fname) 492 | self.force = force 493 | 494 | def check(self): 495 | PYTHONSTARTUP = os.environ.get('PYTHONSTARTUP') 496 | if PYTHONSTARTUP: 497 | return 'PYTHONSTARTUP already defined: %s' % PYTHONSTARTUP 498 | if os.path.exists(self.filename): 499 | return '%s already exists' % self.filename 500 | 501 | def install(self): 502 | import textwrap 503 | error = self.check() 504 | if error and not self.force: 505 | print(error) 506 | print('Use --force to overwrite.') 507 | return False 508 | with open(self.filename, 'w') as f: 509 | f.write(textwrap.dedent(""" 510 | import fancycompleter 511 | fancycompleter.interact(persist_history=True) 512 | """)) 513 | self.set_env_var() 514 | return True 515 | 516 | def set_env_var(self): 517 | if sys.platform == 'win32': 518 | os.system('SETX PYTHONSTARTUP "%s"' % self.filename) 519 | print('%PYTHONSTARTUP% set to', self.filename) 520 | else: 521 | print('startup file written to', self.filename) 522 | print('Append this line to your ~/.bashrc:') 523 | print(' export PYTHONSTARTUP=%s' % self.filename) 524 | 525 | 526 | if __name__ == '__main__': 527 | def usage(): 528 | print('Usage: python -m fancycompleter install [-f|--force]') 529 | sys.exit(1) 530 | 531 | cmd = None 532 | force = False 533 | for item in sys.argv[1:]: 534 | if item in ('install',): 535 | cmd = item 536 | elif item in ('-f', '--force'): 537 | force = True 538 | else: 539 | usage() 540 | # 541 | if cmd == 'install': 542 | installer = Installer('~', force) 543 | installer.install() 544 | else: 545 | usage() 546 | -------------------------------------------------------------------------------- /misc/fancycompleterrc.py: -------------------------------------------------------------------------------- 1 | from fancycompleter import DefaultConfig 2 | 3 | class Config(DefaultConfig): 4 | prefer_pyrepl = False # force fancycompleter to use the standard readline 5 | use_colors = True # you need a patched libreadline for this 6 | -------------------------------------------------------------------------------- /misc/libreadline.so.6-32bit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdbpp/fancycompleter/67e3ec128cf8d44be6e48e775234c07f4b23064e/misc/libreadline.so.6-32bit -------------------------------------------------------------------------------- /misc/libreadline.so.6-64bit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdbpp/fancycompleter/67e3ec128cf8d44be6e48e775234c07f4b23064e/misc/libreadline.so.6-64bit -------------------------------------------------------------------------------- /misc/readline-escape-5.2.patch: -------------------------------------------------------------------------------- 1 | diff -ur readline-5.2/bind.c readline-5.2-hacked/bind.c 2 | --- readline-5.2/bind.c 2006-07-27 15:44:10.000000000 +0200 3 | +++ readline-5.2-hacked/bind.c 2008-06-19 23:23:50.000000000 +0200 4 | @@ -1433,6 +1433,7 @@ 5 | { "print-completions-horizontally", &_rl_print_completions_horizontally, 0 }, 6 | { "show-all-if-ambiguous", &_rl_complete_show_all, 0 }, 7 | { "show-all-if-unmodified", &_rl_complete_show_unmodified, 0 }, 8 | + { "dont-escape-ctrl-chars", &rl_dont_escape_ctrl_chars, 0 }, 9 | #if defined (VISIBLE_STATS) 10 | { "visible-stats", &rl_visible_stats, 0 }, 11 | #endif /* VISIBLE_STATS */ 12 | diff -ur readline-5.2/complete.c readline-5.2-hacked/complete.c 13 | --- readline-5.2/complete.c 2006-07-28 17:35:49.000000000 +0200 14 | +++ readline-5.2-hacked/complete.c 2008-06-20 15:36:48.000000000 +0200 15 | @@ -27,6 +27,7 @@ 16 | 17 | #include 18 | #include 19 | +#include 20 | #if defined (HAVE_SYS_FILE_H) 21 | # include 22 | #endif 23 | @@ -214,6 +215,10 @@ 24 | 25 | int _rl_page_completions = 1; 26 | 27 | +/* Non-zero means that ctrl chars are not replaced by '^' when 28 | + printing the list of possible completions */ 29 | +int rl_dont_escape_ctrl_chars = 0; 30 | + 31 | /* The basic list of characters that signal a break between words for the 32 | completer routine. The contents of this variable is what breaks words 33 | in the shell, i.e. " \t\n\"\\'`@$><=" */ 34 | @@ -568,6 +573,7 @@ 35 | const char *string; 36 | { 37 | int width, pos; 38 | + int escape_seq; 39 | #if defined (HANDLE_MULTIBYTE) 40 | mbstate_t ps; 41 | int left, w; 42 | @@ -578,10 +584,21 @@ 43 | memset (&ps, 0, sizeof (mbstate_t)); 44 | #endif 45 | 46 | - width = pos = 0; 47 | + width = pos = escape_seq = 0; 48 | while (string[pos]) 49 | { 50 | - if (CTRL_CHAR (*string) || *string == RUBOUT) 51 | + if (rl_dont_escape_ctrl_chars && string[pos] == '\033') 52 | + { 53 | + escape_seq = 1; 54 | + pos++; 55 | + } 56 | + else if (rl_dont_escape_ctrl_chars && escape_seq) 57 | + { 58 | + if (isalpha(string[pos])) 59 | + escape_seq = 0; // escape sequence ended 60 | + pos++; 61 | + } 62 | + else if (CTRL_CHAR (string[pos]) || string[pos] == RUBOUT) 63 | { 64 | width += 2; 65 | pos++; 66 | @@ -635,7 +652,7 @@ 67 | s = to_print; 68 | while (*s) 69 | { 70 | - if (CTRL_CHAR (*s)) 71 | + if (!rl_dont_escape_ctrl_chars && CTRL_CHAR (*s)) 72 | { 73 | putc ('^', rl_outstream); 74 | putc (UNCTRL (*s), rl_outstream); 75 | @@ -698,7 +715,8 @@ 76 | char *s, c, *new_full_pathname, *dn; 77 | 78 | extension_char = 0; 79 | - printed_len = fnprint (to_print); 80 | + fnprint (to_print); 81 | + printed_len = fnwidth (to_print); 82 | 83 | #if defined (VISIBLE_STATS) 84 | if (rl_filename_completion_desired && (rl_visible_stats || _rl_complete_mark_directories)) 85 | diff -ur readline-5.2/rlprivate.h readline-5.2-hacked/rlprivate.h 86 | --- readline-5.2/rlprivate.h 2006-07-18 16:36:32.000000000 +0200 87 | +++ readline-5.2-hacked/rlprivate.h 2008-06-19 21:50:19.000000000 +0200 88 | @@ -131,6 +131,7 @@ 89 | 90 | /* complete.c */ 91 | extern int rl_complete_with_tilde_expansion; 92 | +extern int rl_dont_escape_ctrl_chars; 93 | #if defined (VISIBLE_STATS) 94 | extern int rl_visible_stats; 95 | #endif /* VISIBLE_STATS */ 96 | -------------------------------------------------------------------------------- /misc/readline-escape-6.0.patch: -------------------------------------------------------------------------------- 1 | diff -u readline6-6.0/bind.c readline6-6.0-hacked/bind.c 2 | --- readline6-6.0/bind.c 2009-01-23 02:15:57.000000000 +0100 3 | +++ readline6-6.0-hacked/bind.c 2009-10-19 11:21:15.796474119 +0200 4 | @@ -1436,6 +1436,7 @@ 5 | { "revert-all-at-newline", &_rl_revert_all_at_newline, 0 }, 6 | { "show-all-if-ambiguous", &_rl_complete_show_all, 0 }, 7 | { "show-all-if-unmodified", &_rl_complete_show_unmodified, 0 }, 8 | + { "dont-escape-ctrl-chars", &rl_dont_escape_ctrl_chars, 0 }, 9 | #if defined (VISIBLE_STATS) 10 | { "visible-stats", &rl_visible_stats, 0 }, 11 | #endif /* VISIBLE_STATS */ 12 | diff -u readline6-6.0/complete.c readline6-6.0-hacked/complete.c 13 | --- readline6-6.0/complete.c 2009-01-22 21:15:14.000000000 +0100 14 | +++ readline6-6.0-hacked/complete.c 2009-10-19 11:25:13.157653382 +0200 15 | @@ -27,6 +27,7 @@ 16 | 17 | #include 18 | #include 19 | +#include 20 | #if defined (HAVE_SYS_FILE_H) 21 | # include 22 | #endif 23 | @@ -224,6 +225,10 @@ 24 | 25 | int _rl_page_completions = 1; 26 | 27 | +/* Non-zero means that ctrl chars are not replaced by '^' when 28 | + printing the list of possible completions */ 29 | +int rl_dont_escape_ctrl_chars = 0; 30 | + 31 | /* The basic list of characters that signal a break between words for the 32 | completer routine. The contents of this variable is what breaks words 33 | in the shell, i.e. " \t\n\"\\'`@$><=" */ 34 | @@ -607,6 +612,7 @@ 35 | const char *string; 36 | { 37 | int width, pos; 38 | + int escape_seq; 39 | #if defined (HANDLE_MULTIBYTE) 40 | mbstate_t ps; 41 | int left, w; 42 | @@ -617,10 +623,22 @@ 43 | memset (&ps, 0, sizeof (mbstate_t)); 44 | #endif 45 | 46 | - width = pos = 0; 47 | + width = pos = escape_seq = 0; 48 | while (string[pos]) 49 | { 50 | - if (CTRL_CHAR (string[pos]) || string[pos] == RUBOUT) 51 | + if (rl_dont_escape_ctrl_chars && string[pos] == '\033') 52 | + { 53 | + escape_seq = 1; 54 | + pos++; 55 | + } 56 | + else if (rl_dont_escape_ctrl_chars && escape_seq) 57 | + { 58 | + if (isalpha(string[pos])) 59 | + escape_seq = 0; // escape sequence ended 60 | + pos++; 61 | + } 62 | + else if (CTRL_CHAR (string[pos]) || string[pos] == RUBOUT) 63 | + 64 | { 65 | width += 2; 66 | pos++; 67 | @@ -693,7 +711,7 @@ 68 | s = to_print + prefix_bytes; 69 | while (*s) 70 | { 71 | - if (CTRL_CHAR (*s)) 72 | + if (!rl_dont_escape_ctrl_chars && CTRL_CHAR (*s)) 73 | { 74 | putc ('^', rl_outstream); 75 | putc (UNCTRL (*s), rl_outstream); 76 | @@ -757,7 +775,8 @@ 77 | char *s, c, *new_full_pathname, *dn; 78 | 79 | extension_char = 0; 80 | - printed_len = fnprint (to_print, prefix_bytes); 81 | + fnprint (to_print, prefix_bytes); 82 | + printed_len = fnwidth (to_print); 83 | 84 | #if defined (VISIBLE_STATS) 85 | if (rl_filename_completion_desired && (rl_visible_stats || _rl_complete_mark_directories)) 86 | diff -u readline6-6.0/rlprivate.h readline6-6.0-hacked/rlprivate.h 87 | --- readline6-6.0/rlprivate.h 2009-01-23 03:56:49.000000000 +0100 88 | +++ readline6-6.0-hacked/rlprivate.h 2009-10-19 11:21:15.796474119 +0200 89 | @@ -145,6 +145,7 @@ 90 | 91 | /* complete.c */ 92 | extern int rl_complete_with_tilde_expansion; 93 | +extern int rl_dont_escape_ctrl_chars; 94 | #if defined (VISIBLE_STATS) 95 | extern int rl_visible_stats; 96 | #endif /* VISIBLE_STATS */ 97 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdbpp/fancycompleter/67e3ec128cf8d44be6e48e775234c07f4b23064e/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='fancycompleter', 5 | setup_requires="setupmeta", 6 | versioning="devcommit", 7 | maintainer="Daniel Hahler", 8 | url='https://github.com/pdbpp/fancycompleter', 9 | author='Antonio Cuni', 10 | author_email='anto.cuni@gmail.com', 11 | py_modules=['fancycompleter'], 12 | license='BSD', 13 | description='colorful TAB completion for Python prompt', 14 | keywords='rlcompleter prompt tab color completion', 15 | classifiers=[ 16 | "Development Status :: 4 - Beta", 17 | "Environment :: Console", 18 | "License :: OSI Approved :: BSD License", 19 | 'Programming Language :: Python :: 2.6', 20 | 'Programming Language :: Python :: 2.7', 21 | 'Programming Language :: Python :: 3.5', 22 | 'Programming Language :: Python :: 3.6', 23 | 'Programming Language :: Python :: 3.7', 24 | 'Programming Language :: Python :: 3.8', 25 | "Intended Audience :: Developers", 26 | "Operating System :: POSIX", 27 | "Operating System :: Microsoft :: Windows", 28 | "Topic :: Utilities", 29 | ], 30 | install_requires=[ 31 | "pyrepl @ git+https://github.com/pdbpp/pyrepl@master#egg=pyrepl", 32 | "pyreadline;platform_system=='Windows'", 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdbpp/fancycompleter/67e3ec128cf8d44be6e48e775234c07f4b23064e/testing/__init__.py -------------------------------------------------------------------------------- /testing/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | pytest_plugins = ["pytester"] 5 | 6 | 7 | @pytest.fixture 8 | def tmphome(tmpdir, monkeypatch): 9 | monkeypatch.setenv("HOME", str(tmpdir)) 10 | monkeypatch.setenv("USERPROFILE", str(tmpdir)) 11 | 12 | with tmpdir.as_cwd(): 13 | yield tmpdir 14 | -------------------------------------------------------------------------------- /testing/test_configurableclass.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from fancycompleter import ConfigurableClass 4 | 5 | 6 | def test_config(tmphome, capsys, LineMatcher): 7 | class DefaultCfg: 8 | default = 42 9 | 10 | class MyCfg(ConfigurableClass): 11 | DefaultConfig = DefaultCfg 12 | config_filename = ".mycfg" 13 | 14 | cfg = MyCfg() 15 | 16 | # Calls passed in Config. 17 | assert cfg.get_config(Config=lambda: "42") == "42" 18 | 19 | # Uses DefaultConfig instance. 20 | assert isinstance(cfg.get_config(None), DefaultCfg) 21 | 22 | cfgfile = tmphome.join(MyCfg.config_filename) 23 | 24 | # Handles empty config file (missing "Config"). 25 | cfgfile.ensure() 26 | assert isinstance(cfg.get_config(None), DefaultCfg) 27 | out, err = capsys.readouterr() 28 | assert out == "" 29 | assert err == "" 30 | 31 | # Exception when instantiating Config. 32 | p = os.path.normpath(str(tmphome.ensure(MyCfg.config_filename))) 33 | cfgfile.write("def Config(): raise Exception('my_exc')") 34 | assert isinstance(cfg.get_config(None), DefaultCfg) 35 | out, err = capsys.readouterr() 36 | assert out == "" 37 | assert err == ( 38 | "** error when setting up Config from ~/.mycfg: my_exc (%s:1) **\n" % p 39 | ) 40 | 41 | # Error during execfile. 42 | tmphome.ensure(MyCfg.config_filename) 43 | cfgfile.write("raise Exception('my_execfile_exc')") 44 | assert isinstance(cfg.get_config(None), DefaultCfg) 45 | out, err = capsys.readouterr() 46 | assert out == "" 47 | LineMatcher(err.splitlines()).fnmatch_lines([ 48 | "[*][*] error when importing ~/.mycfg: Exception('my_execfile_exc'*) [*][*]", 49 | ' File */fancycompleter.py", line *, in get_config', 50 | ' my_execfile(rcfile, mydict)', 51 | ' File */fancycompleter.py", line *, in my_execfile', 52 | ' exec(code, mydict)', 53 | ' File "*/test_config0/.mycfg", line 1, in ', 54 | " raise Exception('my_execfile_exc')", 55 | ]) 56 | -------------------------------------------------------------------------------- /testing/test_fancycompleter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from fancycompleter import (Color, Completer, DefaultConfig, Installer, 4 | LazyVersion, commonprefix) 5 | 6 | 7 | class ConfigForTest(DefaultConfig): 8 | use_colors = False 9 | 10 | 11 | class ColorConfig(DefaultConfig): 12 | use_colors = True 13 | 14 | 15 | def test_commonprefix(): 16 | assert commonprefix(['isalpha', 'isdigit', 'foo']) == '' 17 | assert commonprefix(['isalpha', 'isdigit']) == 'is' 18 | assert commonprefix(['isalpha', 'isdigit', 'foo'], base='i') == 'is' 19 | assert commonprefix([]) == '' 20 | assert commonprefix(['aaa', 'bbb'], base='x') == '' 21 | 22 | 23 | def test_complete_attribute(): 24 | compl = Completer({'a': None}, ConfigForTest) 25 | assert compl.attr_matches('a.') == ['a.__'] 26 | matches = compl.attr_matches('a.__') 27 | assert 'a.__class__' not in matches 28 | assert '__class__' in matches 29 | assert compl.attr_matches('a.__class') == ['a.__class__'] 30 | 31 | 32 | def test_complete_attribute_prefix(): 33 | class C(object): 34 | attr = 1 35 | _attr = 2 36 | __attr__attr = 3 37 | compl = Completer({'a': C}, ConfigForTest) 38 | assert compl.attr_matches('a.') == ['attr', 'mro'] 39 | assert compl.attr_matches('a._') == ['_C__attr__attr', '_attr', ' '] 40 | matches = compl.attr_matches('a.__') 41 | assert 'a.__class__' not in matches 42 | assert '__class__' in matches 43 | assert compl.attr_matches('a.__class') == ['a.__class__'] 44 | 45 | compl = Completer({'a': None}, ConfigForTest) 46 | assert compl.attr_matches('a._') == ['a.__'] 47 | 48 | 49 | def test_complete_attribute_colored(): 50 | compl = Completer({'a': 42}, ColorConfig) 51 | matches = compl.attr_matches('a.__') 52 | assert len(matches) > 2 53 | expected_color = compl.config.color_by_type.get(type(compl.__class__)) 54 | assert expected_color == '35;01' 55 | expected_part = Color.set(expected_color, '__class__') 56 | for match in matches: 57 | if expected_part in match: 58 | break 59 | else: 60 | assert False, matches 61 | assert ' ' in matches 62 | 63 | 64 | def test_complete_colored_single_match(): 65 | """No coloring, via commonprefix.""" 66 | compl = Completer({'foobar': 42}, ColorConfig) 67 | matches = compl.global_matches('foob') 68 | assert matches == ['foobar'] 69 | 70 | 71 | def test_does_not_color_single_match(): 72 | class obj: 73 | msgs = [] 74 | 75 | compl = Completer({'obj': obj}, ColorConfig) 76 | matches = compl.attr_matches('obj.msgs') 77 | assert matches == ['obj.msgs'] 78 | 79 | 80 | def test_complete_global(): 81 | compl = Completer({'foobar': 1, 'foobazzz': 2}, ConfigForTest) 82 | assert compl.global_matches('foo') == ['fooba'] 83 | matches = compl.global_matches('fooba') 84 | assert set(matches) == set(['foobar', 'foobazzz']) 85 | assert compl.global_matches('foobaz') == ['foobazzz'] 86 | assert compl.global_matches('nothing') == [] 87 | 88 | 89 | def test_complete_global_colored(): 90 | compl = Completer({'foobar': 1, 'foobazzz': 2}, ColorConfig) 91 | assert compl.global_matches('foo') == ['fooba'] 92 | matches = compl.global_matches('fooba') 93 | assert set(matches) == { 94 | ' ', 95 | '\x1b[001;00m\x1b[33;01mfoobazzz\x1b[00m', 96 | '\x1b[000;00m\x1b[33;01mfoobar\x1b[00m', 97 | } 98 | assert compl.global_matches('foobaz') == ['foobazzz'] 99 | assert compl.global_matches('nothing') == [] 100 | 101 | 102 | def test_complete_global_colored_exception(): 103 | compl = Completer({'tryme': ValueError()}, ColorConfig) 104 | if sys.version_info >= (3, 6): 105 | assert compl.global_matches('try') == [ 106 | '\x1b[000;00m\x1b[37mtry:\x1b[00m', 107 | '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', 108 | ' ' 109 | ] 110 | else: 111 | assert compl.global_matches('try') == [ 112 | '\x1b[000;00m\x1b[37mtry\x1b[00m', 113 | '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', 114 | ' ' 115 | ] 116 | 117 | 118 | def test_complete_global_exception(monkeypatch): 119 | import rlcompleter 120 | 121 | def rlcompleter_global_matches(self, text): 122 | return ['trigger_exception!', 'nameerror', 'valid'] 123 | 124 | monkeypatch.setattr(rlcompleter.Completer, 'global_matches', 125 | rlcompleter_global_matches) 126 | 127 | compl = Completer({'valid': 42}, ColorConfig) 128 | assert compl.global_matches("") == [ 129 | "\x1b[000;00m\x1b[31;01mnameerror\x1b[00m", 130 | "\x1b[001;00m\x1b[31;01mtrigger_exception!\x1b[00m", 131 | "\x1b[002;00m\x1b[33;01mvalid\x1b[00m", 132 | " ", 133 | ] 134 | 135 | 136 | def test_color_for_obj(monkeypatch): 137 | class Config(ColorConfig): 138 | color_by_type = {} 139 | 140 | compl = Completer({}, Config) 141 | assert compl.color_for_obj(1, "foo", "bar") == "\x1b[001;00m\x1b[00mfoo\x1b[00m" 142 | 143 | 144 | def test_complete_with_indexer(): 145 | compl = Completer({'lst': [None, 2, 3]}, ConfigForTest) 146 | assert compl.attr_matches('lst[0].') == ['lst[0].__'] 147 | matches = compl.attr_matches('lst[0].__') 148 | assert 'lst[0].__class__' not in matches 149 | assert '__class__' in matches 150 | assert compl.attr_matches('lst[0].__class') == ['lst[0].__class__'] 151 | 152 | 153 | def test_autocomplete(): 154 | class A: 155 | aaa = None 156 | abc_1 = None 157 | abc_2 = None 158 | abc_3 = None 159 | bbb = None 160 | compl = Completer({'A': A}, ConfigForTest) 161 | # 162 | # in this case, we want to display all attributes which start with 163 | # 'a'. MOREOVER, we also include a space to prevent readline to 164 | # automatically insert the common prefix (which will the the ANSI escape 165 | # sequence if we use colors) 166 | matches = compl.attr_matches('A.a') 167 | assert sorted(matches) == [' ', 'aaa', 'abc_1', 'abc_2', 'abc_3'] 168 | # 169 | # IF there is an actual common prefix, we return just it, so that readline 170 | # will insert it into place 171 | matches = compl.attr_matches('A.ab') 172 | assert matches == ['A.abc_'] 173 | # 174 | # finally, at the next TAB, we display again all the completions available 175 | # for this common prefix. Agai, we insert a spurious space to prevent the 176 | # automatic completion of ANSI sequences 177 | matches = compl.attr_matches('A.abc_') 178 | assert sorted(matches) == [' ', 'abc_1', 'abc_2', 'abc_3'] 179 | 180 | 181 | def test_complete_exception(): 182 | compl = Completer({}, ConfigForTest) 183 | assert compl.attr_matches('xxx.') == [] 184 | 185 | 186 | def test_complete_invalid_attr(): 187 | compl = Completer({'str': str}, ConfigForTest) 188 | assert compl.attr_matches('str.xx') == [] 189 | 190 | 191 | def test_complete_function_skipped(): 192 | compl = Completer({'str': str}, ConfigForTest) 193 | assert compl.attr_matches('str.split().') == [] 194 | 195 | 196 | def test_unicode_in___dir__(): 197 | class Foo(object): 198 | def __dir__(self): 199 | return [u'hello', 'world'] 200 | 201 | compl = Completer({'a': Foo()}, ConfigForTest) 202 | matches = compl.attr_matches('a.') 203 | assert matches == ['hello', 'world'] 204 | assert type(matches[0]) is str 205 | 206 | 207 | class MyInstaller(Installer): 208 | env_var = 0 209 | 210 | def set_env_var(self): 211 | self.env_var += 1 212 | 213 | 214 | class TestInstaller(object): 215 | 216 | def test_check(self, monkeypatch, tmpdir): 217 | installer = MyInstaller(str(tmpdir), force=False) 218 | monkeypatch.setenv('PYTHONSTARTUP', '') 219 | assert installer.check() is None 220 | f = tmpdir.join('python_startup.py').ensure(file=True) 221 | assert installer.check() == '%s already exists' % f 222 | monkeypatch.setenv('PYTHONSTARTUP', 'foo') 223 | assert installer.check() == 'PYTHONSTARTUP already defined: foo' 224 | 225 | def test_install(self, monkeypatch, tmpdir): 226 | installer = MyInstaller(str(tmpdir), force=False) 227 | monkeypatch.setenv('PYTHONSTARTUP', '') 228 | assert installer.install() 229 | assert 'fancycompleter' in tmpdir.join('python_startup.py').read() 230 | assert installer.env_var == 1 231 | # 232 | # the second time, it fails because the file already exists 233 | assert not installer.install() 234 | assert installer.env_var == 1 235 | # 236 | # the third time, it succeeds because we set force 237 | installer.force = True 238 | assert installer.install() 239 | assert installer.env_var == 2 240 | 241 | 242 | class TestLazyVersion(object): 243 | 244 | class MyLazyVersion(LazyVersion): 245 | __count = 0 246 | 247 | def _load_version(self): 248 | assert self.__count == 0 249 | self.__count += 1 250 | return '0.1' 251 | 252 | def test_lazy_version(self): 253 | ver = self.MyLazyVersion('foo') 254 | assert repr(ver) == '0.1' 255 | assert str(ver) == '0.1' 256 | assert ver == '0.1' 257 | assert not ver != '0.1' 258 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35,36,37,38,py,py3}, checkqa 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | coverage: pytest-cov 8 | commands = pytest {posargs} 9 | setenv = 10 | coverage: PYTEST_ADDOPTS=--cov --cov-report=term-missing {env:PYTEST_ADDOPTS:} 11 | 12 | [testenv:checkqa] 13 | deps = 14 | flake8 15 | commands = flake8 setup.py fancycompleter.py testing 16 | 17 | [pytest] 18 | addopts = -ra 19 | testpaths = testing 20 | 21 | [coverage:run] 22 | include = */fancycompleter.py, testing/* 23 | parallel = 1 24 | branch = 1 25 | 26 | [coverage:paths] 27 | source = . 28 | */lib/python*/site-packages/ 29 | */pypy*/site-packages/ 30 | *\Lib\site-packages\ 31 | 32 | [flake8] 33 | max-line-length = 88 34 | --------------------------------------------------------------------------------