├── .gitignore
├── AUTHORS.txt
├── CHANGELOG
├── CONTRIBUTING.rst
├── INSTALL.rst
├── LICENSE.txt
├── README.rst
├── call_map
├── __init__.py
├── __main__.py
├── cache.py
├── config.py
├── core.py
├── custom_typing.py
├── errors.py
├── gui.py
├── icons
│ ├── cruciform-by-andylee.png
│ └── statement-of-permission.txt
├── jedi_alt
│ ├── AUTHORS.txt
│ ├── JEDI_LICENSE.txt
│ ├── __init__.py
│ ├── api_usages.py
│ ├── monkey_patch_evaluate_compiled.py
│ ├── stop_signal.py
│ └── usages.py
├── jedi_ast_tools.py
├── jedi_dump.py
├── package_info.json
├── project_settings_module.py
├── qt_compatibility.py
├── serialize.py
└── wheel_fix.py
├── dev_helper_tools
└── shell_tools.zsh
├── docs
└── UI-clicked-on-function.png
├── gpl-3.0.txt
├── optional-requirements.txt
├── setup.py
└── tests
├── load_test_modules.py
├── test_cache.py
├── test_jedi_ast_tools.py
├── test_jedi_dump.py
├── test_modules
├── simple_test_package
│ ├── __init__.py
│ ├── aa.py
│ └── bb.py
├── test_package_sibling_usage
│ ├── __init__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ └── test_sibling_usage.py
│ └── test_sibling_used.py
├── use_comprehension.py
└── use_decorators.py
├── test_project.py
└── test_ui.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | __pycache__
3 | .cm_proj
4 | local-meta/
5 |
--------------------------------------------------------------------------------
/AUTHORS.txt:
--------------------------------------------------------------------------------
1 | # Call Map authors
2 |
3 | Andrew J. Lee @andylee-ncc, @andylee-work, @ajylee
4 |
5 |
6 | # Jedi authors
7 |
8 | See call_map/jedi_alt/AUTHORS.txt
9 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | # v0.6.0 -- 2017-05-04
2 |
3 | Make Call Map compatible with official Jedi v0.10.2 release.
4 |
5 | # v0.5.0
6 |
7 | Make Call Map compatible with official Jedi v0.10.0 release. Previously it
8 | depended on a custom fork.
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | Licensing/copyright issues
2 | ==========================
3 |
4 | Note that I (Andy Lee) am planning to make a release of Call Map compatible
5 | with PySide2, licensed under BSD 2-clause license. To contribute code to the
6 | Call Map program, you must agree to allow your contribution to be licensed
7 | under GPL-v3, and also BSD 2-clause license as part of any future release the
8 | Call Map project that is PySide2-compatible.
9 |
10 | Call Map is copyrighted to its developers, so that will include you if you
11 | contribute.
12 |
--------------------------------------------------------------------------------
/INSTALL.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Installation
3 | ============
4 |
5 | .. default-role:: code
6 |
7 |
8 | Standard Installation with Pip
9 | -------------------------------
10 |
11 | Call Map is distributed as a Python package. It requires `Python 3.5`,
12 | `pip>=8.1.2`, and `jedi==0.10.0` [*]_. Other requirements can be installed
13 | automatically once you have the correct versions of `pip` and `jedi`.
14 | (If you don't have `Python 3.5`, see `Getting Python 3.5`_.)
15 |
16 | Before installing for the first time, consider `Using Virtualenv`_.
17 | To install `call_map`, from the top level directory of `call_map`, run::
18 |
19 | pip3 install --upgrade pip # make sure pip is >=8.1.2
20 | pip3 install --no-cache-dir -e . # caching sometimes breaks
21 |
22 | Note that some Python 3 distributions call the package manager `pip`, and some
23 | call it `pip3`.
24 |
25 | I have included instructions using `conda`, `apt`, and `brew` if you prefer
26 | obtaining as many depencencies as possible using those package managers. I also
27 | include instructions for obtaining `Python 3.5` with `apt` and `brew`.
28 |
29 | More details about `PyQt5` specifically can be found at `the Riverbank download
30 | page`__ (Riverbank is the developer of `PyQt5`).
31 |
32 | __ https://www.riverbankcomputing.com/software/pyqt/download5
33 |
34 | .. [*] A small part of `call_map` relies on internal structures of `jedi`, which
35 | may change in future releases. I plan to make `call_map` rely less on
36 | `jedi` internals in future `call_map` releases.
37 |
38 |
39 | Using Virtualenv
40 | -----------------
41 |
42 | Setting up virtual environment is optional but recommended. Virtualenv makes it
43 | easy to start over if things break, by deleting the virtualenv directory. To
44 | set it up, run::
45 |
46 | python -m venv --system-site-packages $HOME/py3env
47 | export PATH=$HOME/py3env/bin:$PATH
48 |
49 | # update command hash table in bash/zsh
50 | hash pip3
51 | hash python
52 |
53 |
54 | Getting Python 3.5
55 | -------------------
56 |
57 | You can obtain the `official release`__ or use a package manager. Specific
58 | instructions for Mac OS and Ubuntu are included below.
59 |
60 | __ https://www.python.org/downloads/release/python-352/
61 |
62 | Mac OS
63 | ~~~~~~~
64 |
65 | You can use `brew`::
66 |
67 | brew install python3
68 |
69 | You can also use `brew` to compile PyQt5::
70 |
71 | brew install --with-python3 pyqt5
72 |
73 |
74 | Ubuntu
75 | ~~~~~~~
76 |
77 | You can use `apt` to install `Python 3.5` and `PyQt5`::
78 |
79 | sudo apt install python3-pyqt5 virtualenv
80 |
81 |
82 | Using Conda
83 | ------------
84 |
85 | You can use `conda` to install `pyqt` and `qtconsole`, then `pip` to install
86 | `jedi` and `call_map`::
87 |
88 | pip install conda
89 | hash conda
90 | conda install pygments
91 | conda install pyqt
92 | conda install qtconsole
93 | conda install toolz
94 |
95 | Now follow the `pip` installation instructions in `Standard Installation with
96 | Pip`_. `pip` will not reinstall the packages you installed with conda.
97 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | # Jedi License Terms
2 |
3 | Some code from the Jedi project is included in modified form in
4 |
5 | - call_map/jedi_alt/api_usages.py
6 | - call_map/jedi_alt/usages.py
7 |
8 | This code is dual licensed under the terms of the original Jedi license and the
9 | Call Map Program License Terms.
10 |
11 | Jedi is Copyright (C) David Halter and others, see call_map/jedi_alt/AUTHORS.txt.
12 | Also see call_map/jedi_alt/JEDI_LICENSE.txt for the original Jedi license.
13 |
14 |
15 | # Call Map Program License Terms
16 |
17 | Copyright (C) 2016-2017 Call Map developers
18 |
19 | In the following, "this program" refers to the source code and associated
20 | documentation files. "This program" does not include the icons.
21 |
22 | ---
23 |
24 |
25 | This program is free software: you can redistribute it and/or modify
26 | it under the terms of the GNU General Public License as published by
27 | the Free Software Foundation, either version 3 of the License, or
28 | (at your option) any later version.
29 |
30 | This program is distributed in the hope that it will be useful,
31 | but WITHOUT ANY WARRANTY; without even the implied warranty of
32 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 | GNU General Public License for more details.
34 |
35 | You should have received a copy of the GNU General Public License
36 | along with this program. If not, see .
37 |
38 | ---
39 |
40 | # Image Assets License
41 |
42 | The image assets included in this repository are copyright Andrew J. Lee
43 | (C) and separately licensed, as documented in
44 | call_map/icons/statement-of-permission.txt.
45 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | Call Map
3 | ==========
4 |
5 |
6 | .. default-role:: code
7 |
8 | About
9 | ======
10 |
11 | Call Map is a tool for navigating call graphs in Python, with plans to support
12 | other languages. Below is a screen shot after running `call_map -m toolz`, then
13 | clicking on some functions.
14 |
15 | .. figure:: docs/UI-clicked-on-function.png
16 |
17 | While Call Map is intended to help in gaining a general
18 | understanding of a codebase, it is also a natural fit for tracing code paths,
19 | which constitutes an important security concern. Many security issues are
20 | revealed by finding a code path that connects user input to dangerous coding
21 | patterns.
22 |
23 | See the `blog post`__ for more.
24 |
25 | __ https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2017/july/call-map-a-tool-for-navigating-call-graphs-in-python/
26 |
27 |
28 | Installation
29 | =============
30 |
31 | Call Map is distributed as a python package for `Python 3.5`. To install with
32 | `pip3`, run::
33 |
34 | # From top level directory of call_map
35 | pip3 install -e .
36 |
37 | Note that some Python 3 distributions call the package manager `pip`, and some
38 | call it `pip3`, so you may need substitute `pip3` with `pip`.
39 |
40 | The above commands should install the executable `call_map` to the same path
41 | that the Python interpreter is in.
42 |
43 | For details and alternative methods see `INSTALL.rst`.
44 |
45 |
46 | Quick Start
47 | ============
48 |
49 | Open files to start exploring::
50 |
51 | call_map -f example.py
52 |
53 | # you can also open multiple files
54 | call_map -f *.py
55 |
56 |
57 | You can also add to the module search path (`sys.path`)::
58 |
59 | call_map -f *.py -p .
60 |
61 | Call Map will try to resolve the files as modules whenever they can be found in
62 | the module search path. For more documentation on command line
63 | arguments, `call_map -h`.
64 |
65 |
66 | Configuration
67 | =============
68 |
69 | To configure Call Map, set the environment variable CALL_MAP_RC_DIRECTORY.
70 | The path to the Call Map configuration file will be::
71 |
72 | $CALL_MAP_RC_DIRECTORY/call_map_rc.py
73 |
74 | At this time the configuration options are:
75 |
76 | - `open_in_editor(path: pathlib.Path, line: int)`: if you define this function
77 | it will be called whenever you open a file. For example, the following can be
78 | used to open files in an Emacs server if have you called `server-mode` in a
79 | running Emacs session::
80 |
81 | def open_in_editor(path, line):
82 | import subprocess as sbp
83 | sbp.call(['emacsclient', '+{}'.format(line), str(path)])
84 |
85 | The following can be used to open files in the GVim server if you started a GVim
86 | server with `gvim --servername my_vim_server`::
87 |
88 | def open_in_editor(path, line):
89 | import subprocess as sbp
90 | sbp.call(['gvim', '--servername', 'my_vim_server', '--remote', '+{}'.format(line), str(path)])
91 |
92 |
93 | - `MULTITHREADING`: Whether to use a separate thread for the GUI and searching
94 | the call graph. Defaults to `True`. Turning it off is for debug purposes.
95 |
96 |
97 | Quirks
98 | =======
99 |
100 | There are a couple of quirks in the UI design, due to the fact that I haven't
101 | arrived at a better solution or the tradeoff of additional complexity is
102 | unfavorable.
103 |
104 | - Usages can appear to show up more than once, but actually they are different
105 | usages in the same scope.
106 |
107 | - You may notice some bulitins such as `help`, `id`, and `filter` are ignored.
108 | See `call_map/config.py` in the source code for the full list of ignored
109 | functions.
110 |
111 | - If the position to be highlighted is at the start of a file, it won't be
112 | highlighted. This is because typically only modules and scripts are positioned
113 | at the start of their respective files.
114 |
115 |
116 | Quirks Inherited from Jedi
117 | ---------------------------
118 |
119 | Some quirks are inherited from the `jedi` Python analysis backend.
120 |
121 | - The search scope for usages is the set of modules that have been loaded by the
122 | `jedi` backend. That means that the scope will change as you explore new
123 | modules. If you want to explicitly include a module in the search scope, add
124 | it to the initial list of modules to be inspected.
125 |
126 | - `jedi` always searches the interpreter's `sys.path`, even when it is not
127 | explicitly included. However, it will prioritize the user-defined `sys_path`.
128 |
129 |
130 | Caveats
131 | ========
132 |
133 | Any caveats that exist for static analysis backends also apply to Call Map. (At
134 | this time only the `jedi` backend is integrated into Call Map.)
135 |
136 |
137 | Jedi
138 | -----
139 |
140 | The `jedi` backend for Python analysis does not always find all usages. The
141 | dynamic nature of Python makes it impossible to always determine the definition
142 | of a function with static analysis. Sometimes `jedi` resolves a call to multiple
143 | possible functions. For example `os.path.abspath` depends on the platform. In
144 | this case `call_map` lists both possibilities as `abspath` and `abspath (2)`.
145 | `jedi` also does not resolve all calls and usages. Jedi's own documentation also
146 | has a `list of caveats`__.
147 |
148 | Sometimes when Jedi throws an error when analyzing one item, the error affects
149 | other items. For example, when finding usages, if Jedi raises an error on one
150 | usage, the other usages it has found may be unrecoverable (as of Jedi v0.10.0).
151 |
152 | __ https://jedi.readthedocs.io/en/latest/docs/features.html#caveats
153 |
--------------------------------------------------------------------------------
/call_map/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import json as _json
3 | from pathlib import Path as _Path
4 |
5 | logging.getLogger(__name__).addHandler(logging.NullHandler())
6 |
7 | _package_info = (
8 | _json.loads(_Path(__file__).parent.joinpath('package_info.json').read_text()))
9 |
10 | version = _package_info['version']
11 |
--------------------------------------------------------------------------------
/call_map/__main__.py:
--------------------------------------------------------------------------------
1 |
2 | from .gui import main
3 |
4 | if __name__ == '__main__':
5 | main()
6 |
7 | try:
8 | __IPYTHON__
9 | except NameError:
10 | pass
11 | else:
12 | from .gui import Debug
13 | from sys import modules
14 | ui_toplevel = modules['call_map_ui_toplevel']
15 |
--------------------------------------------------------------------------------
/call_map/cache.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | TEXT_CACHE_SIZE = 100
4 | _text_cache = {}
5 | _text_cache_rankings = {}
6 |
7 | def read_text_cached(path: Path) -> str:
8 | try:
9 | text = _text_cache[path]
10 | hole = _text_cache_rankings[path]
11 | eliminate = True
12 | except KeyError:
13 | text = path.read_text()
14 | _text_cache[path] = text
15 | hole = TEXT_CACHE_SIZE
16 | eliminate = False
17 |
18 | _text_cache_rankings[path] = 0
19 |
20 | for key, ranking in list(_text_cache_rankings.items()):
21 | if key == path:
22 | continue
23 |
24 | if ranking < hole:
25 | new_ranking = ranking + 1
26 | if new_ranking < TEXT_CACHE_SIZE:
27 | _text_cache_rankings[key] = new_ranking
28 | else:
29 | _text_cache.pop(key)
30 | _text_cache_rankings.pop(key)
31 |
32 | #print('-'*10, list(_text_cache_rankings.values()), len(_text_cache), len(_text_cache_rankings))
33 |
34 | return text
--------------------------------------------------------------------------------
/call_map/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 | from .qt_compatibility import QtCore
4 | from typing import Tuple, Optional
5 | import toolz as tz
6 |
7 | # ignore
8 |
9 | py_ignore = {'builtins': frozenset(['ArithmeticError',
10 | 'AssertionError',
11 | 'AttributeError',
12 | 'BaseException',
13 | 'BlockingIOError',
14 | 'BrokenPipeError',
15 | 'BufferError',
16 | 'BytesWarning',
17 | 'ChildProcessError',
18 | 'ConnectionAbortedError',
19 | 'ConnectionError',
20 | 'ConnectionRefusedError',
21 | 'ConnectionResetError',
22 | 'DeprecationWarning',
23 | 'EOFError',
24 | 'Ellipsis',
25 | 'EnvironmentError',
26 | 'Exception',
27 | 'False',
28 | 'FileExistsError',
29 | 'FileNotFoundError',
30 | 'FloatingPointError',
31 | 'FutureWarning',
32 | 'GeneratorExit',
33 | 'IOError',
34 | 'ImportError',
35 | 'ImportWarning',
36 | 'IndentationError',
37 | 'IndexError',
38 | 'InterruptedError',
39 | 'IsADirectoryError',
40 | 'KeyError',
41 | 'KeyboardInterrupt',
42 | 'LookupError',
43 | 'MemoryError',
44 | 'NameError',
45 | 'None',
46 | 'NotADirectoryError',
47 | 'NotImplemented',
48 | 'NotImplementedError',
49 | 'OSError',
50 | 'OverflowError',
51 | 'PendingDeprecationWarning',
52 | 'PermissionError',
53 | 'ProcessLookupError',
54 | 'RecursionError',
55 | 'ReferenceError',
56 | 'ResourceWarning',
57 | 'RuntimeError',
58 | 'RuntimeWarning',
59 | 'StopAsyncIteration',
60 | 'StopIteration',
61 | 'SyntaxError',
62 | 'SyntaxWarning',
63 | 'SystemError',
64 | 'SystemExit',
65 | 'TabError',
66 | 'TimeoutError',
67 | 'True',
68 | 'TypeError',
69 | 'UnboundLocalError',
70 | 'UnicodeDecodeError',
71 | 'UnicodeEncodeError',
72 | 'UnicodeError',
73 | 'UnicodeTranslateError',
74 | 'UnicodeWarning',
75 | 'UserWarning',
76 | 'ValueError',
77 | 'Warning',
78 | 'ZeroDivisionError',
79 | '__IPYTHON__',
80 | '__build_class__',
81 | '__debug__',
82 | '__doc__',
83 | '__import__',
84 | '__loader__',
85 | '__name__',
86 | '__package__',
87 | '__spec__',
88 | 'abs',
89 | 'all',
90 | 'any',
91 | 'ascii',
92 | 'bin',
93 | 'bool',
94 | #'bytearray',
95 | #'bytes',
96 | 'callable',
97 | 'chr',
98 | 'classmethod',
99 | #'compile',
100 | 'complex',
101 | 'copyright',
102 | 'credits',
103 | #'delattr',
104 | 'dict',
105 | 'dir',
106 | #'divmod',
107 | #'dreload',
108 | 'enumerate',
109 | #'eval',
110 | #'exec',
111 | 'filter',
112 | 'float',
113 | #'format',
114 | 'frozenset',
115 | #'get_ipython',
116 | #'getattr',
117 | #'globals',
118 | #'hasattr',
119 | 'hash',
120 | 'help',
121 | 'hex',
122 | 'id',
123 | 'input',
124 | 'int',
125 | 'isinstance',
126 | 'issubclass',
127 | 'iter',
128 | 'len',
129 | 'license',
130 | 'list',
131 | #'locals',
132 | 'map',
133 | 'max',
134 | #'memoryview',
135 | 'min',
136 | 'next',
137 | 'object',
138 | 'oct',
139 | #'open',
140 | 'ord',
141 | 'pow',
142 | 'print',
143 | 'property',
144 | 'range',
145 | 'repr',
146 | 'reversed',
147 | 'round',
148 | 'set',
149 | 'setattr',
150 | 'slice',
151 | 'sorted',
152 | 'staticmethod',
153 | 'str',
154 | 'sum',
155 | 'super',
156 | 'tuple',
157 | 'type',
158 | #'vars',
159 | 'zip'
160 | ])}
161 |
162 |
163 |
164 | def default_open_in_editor(path, line):
165 | import sys
166 | import logging
167 | import subprocess as sbp
168 |
169 | if sys.platform == 'darwin':
170 | sbp.call(['open', '--', path])
171 | else:
172 | logging.getLogger(__name__).warning(
173 | ' Need to define open_in_editor in call_map_rc.py')
174 |
175 |
176 | class UserConfig:
177 | """
178 | Manages user config
179 |
180 | The config file at `$CALL_MAP_RC_DIRECTORY/call_map_rc.py` is watched. If
181 | the config file is changed, it will be re-read the next time `get_config` is
182 | called.
183 |
184 | There are three priority levels for settings: defaults, settings from the
185 | config file, and the override settings. The overrides are for testing. The
186 | user also may be allowed to change the overrides mid-session in the future.
187 |
188 | The main reason UserConfig is a class, and not just a collection of
189 | variables and functions, is for testability.
190 |
191 | """
192 |
193 | default_user_config = {'open_in_editor': default_open_in_editor,
194 | 'MULTITHREADING': True,
195 | 'UNICODE_ROLE_MARKERS': True,
196 | 'EXC_INFO': False,
197 | 'EXPERIMENTAL_MODE': False,
198 | 'LOG_LEVEL': None, # needs restart to take effect
199 | 'PROFILING': False} # needs restart to take effect
200 |
201 | def __init__(self, rc_dir: Optional[str]):
202 | self.rc_dir = rc_dir
203 |
204 | self._cache = None # caches the settings
205 |
206 | self.watcher = QtCore.QFileSystemWatcher()
207 | self.watcher.fileChanged.connect(self.clear_cache)
208 | self.start_watcher()
209 |
210 | # top priority configuration settings. Used for debugging.
211 | self.session_overrides = {}
212 |
213 | def clear_cache(self, file_names: Tuple[str]):
214 | self._cache = None
215 |
216 | def start_watcher(self):
217 | if self.rc_dir and os.path.isdir(self.rc_dir):
218 | self.watcher.addPath(os.path.join(self.rc_dir, 'call_map_rc.py'))
219 |
220 | def read_user_config(self):
221 | """Read the configuration from file"""
222 | from sys import path as sys_path
223 |
224 | if self.rc_dir and os.path.isdir(self.rc_dir):
225 | text = Path(self.rc_dir).joinpath('call_map_rc.py').read_text()
226 |
227 | _namespace = {}
228 |
229 | exec(text, _namespace)
230 |
231 | result = {}
232 |
233 | for key in self.default_user_config:
234 | try:
235 | result[key] = _namespace[key]
236 | except KeyError:
237 | continue
238 |
239 | return result
240 | else:
241 | return {}
242 |
243 | def get_config(self):
244 | if self._cache is None:
245 | read_result = self.read_user_config()
246 | return tz.merge(self.default_user_config,
247 | read_result,
248 | self.session_overrides)
249 | else:
250 | return self._cache
251 |
252 |
253 | user_config = UserConfig(rc_dir=os.getenv('CALL_MAP_RC_DIRECTORY'))
254 | get_user_config = user_config.get_config
255 |
--------------------------------------------------------------------------------
/call_map/core.py:
--------------------------------------------------------------------------------
1 | import typing
2 | import collections
3 | import abc
4 |
5 | from typing import List, Tuple, Optional
6 | from pathlib import Path
7 |
8 |
9 | LocationType = Tuple[Optional[int], Optional[int]]
10 | CallPosType = Tuple[Optional[str], LocationType, LocationType]
11 |
12 | CodeElement = typing.NamedTuple(
13 | 'CodeElement', [('name', str),
14 | ('type', str), # not sure
15 | ('module', str),
16 | ('role', str),
17 | ('path', Optional[str]),
18 | ('call_pos', CallPosType),
19 | ('start_pos', LocationType),
20 | ('end_pos', LocationType)])
21 |
22 |
23 | class Node(metaclass=abc.ABCMeta):
24 | """Every node must have attributes `name` and `role`
25 |
26 | The Node is responsible for catching all exceptions arising from the
27 | analysis backend when searching for connected nodes.
28 |
29 | """
30 |
31 | @property
32 | @abc.abstractmethod
33 | def parents(self):
34 | pass
35 |
36 | @property
37 | @abc.abstractmethod
38 | def children(self):
39 | pass
40 |
41 | @property
42 | @abc.abstractmethod
43 | def cancel_search(self):
44 | '''Cancel pending search for related nodes'''
45 | pass
46 |
47 |
48 | class OrganizerNode(Node):
49 | def __init__(self, name, parents=None, children=None):
50 | """The parents call the node, children are called by the node.
51 |
52 | If not callable, the Node has no children
53 | """
54 | self._parents = parents if parents is not None else []
55 | self._children = children if children is not None else []
56 | self.code_element = CodeElement(name=name,
57 | type='organizer',
58 | module='[None]',
59 | role='definition',
60 | path=None,
61 | call_pos=(None, (None,None), (None,None)),
62 | start_pos = (None, None),
63 | end_pos = (None, None))
64 |
65 | @property
66 | def parents(self):
67 | return self._parents
68 |
69 | @property
70 | def children(self):
71 | return self._children
72 |
73 | def cancel_search(self):
74 | pass
75 |
76 |
77 | UserScopeSettings = typing.NamedTuple('UserScopeSettings', [('module_names', List[str]),
78 | ('file_names', List[Path]),
79 | ('include_runtime_sys_path', bool),
80 | ('add_to_sys_path', List[Path])])
81 |
82 | ScopeSettings = typing.NamedTuple('ScopeSettings', [('module_names', List[str]),
83 | ('scripts', List[Path]),
84 | ('effective_sys_path', List[Path])])
85 |
--------------------------------------------------------------------------------
/call_map/custom_typing.py:
--------------------------------------------------------------------------------
1 | import toolz as tz
2 | import typing
3 | import abc
4 |
5 |
6 | class TypeSpec(metaclass=abc.ABCMeta):
7 | '''Used to classify objects, but is not an actual Python type
8 |
9 | Instead of `isinstance`, use `matches_spec` with `TypeSpec`s. Does not implement any
10 | other interface.
11 |
12 | For example, this is useful for specifying a list of strings. `TypeSpec`s
13 | differ from type hints, in that they can easily be checked.
14 |
15 | '''
16 |
17 | @abc.abstractmethod
18 | def __matches_spec__(self, obj):
19 | pass
20 |
21 |
22 | def matches_spec(obj: typing.Any, type_spec: typing.Union[type, TypeSpec, typing.Iterable]):
23 | if isinstance(type_spec, type):
24 | return isinstance(obj, type_spec)
25 | elif isinstance(type_spec, TypeSpec):
26 | return type_spec.__matches_spec__(obj)
27 | elif tz.isiterable(type_spec):
28 | return any(matches_spec(obj, elt) for elt in type_spec)
29 |
30 |
31 | class CheckableOptional(TypeSpec):
32 | def __init__(self, arg):
33 | self.nontrivial_type = arg
34 |
35 | def __repr__(self):
36 | return ''.format(repr(self.nontrivial_type))
37 |
38 | def __matches_spec__(self, obj):
39 | return matches_spec(obj, (type(None), self.nontrivial_type))
40 |
41 |
42 | class CheckableDict(TypeSpec):
43 | def __init__(self, types: dict):
44 | self.value_types = types
45 |
46 | def __repr__(self):
47 | return ''.format(repr(self.value_types))
48 |
49 | def __matches_spec__(self, obj):
50 | return (
51 | isinstance(obj, dict)
52 | and set(self.value_types.keys()) == set(obj.keys())
53 | and all(matches_spec(obj[key], val_type)
54 | for key, val_type in self.value_types.items()))
55 |
56 | def new_empty_instance(self):
57 | return {key: None for key in self.value_types}
58 |
59 |
60 | class CheckableList(TypeSpec):
61 | def __init__(self, value_type):
62 | self.value_type = value_type
63 |
64 | def __repr__(self):
65 | return ''.format(repr(self.value_type))
66 |
67 | def __matches_spec__(self, obj):
68 | return (isinstance(obj, list)
69 | and all(matches_spec(elt, self.value_type) for elt in obj))
70 |
71 | def new_empty_instance(self):
72 | return list()
73 |
74 |
75 | class CheckableTuple(TypeSpec):
76 | def __init__(self, *value_types):
77 | self.value_types = value_types
78 |
79 | def __repr__(self):
80 | return ''.format(repr(self.value_types))
81 |
82 | def __matches_spec__(self, obj):
83 | return (isinstance(obj, tuple)
84 | and all(matches_spec(elt, self.value_type) for elt in zip(obj, self.value_types)))
85 |
--------------------------------------------------------------------------------
/call_map/errors.py:
--------------------------------------------------------------------------------
1 |
2 | class BadArgsError(Exception):
3 | pass
4 |
5 | class ModuleResolutionError(Exception):
6 | pass
7 |
8 | class ScriptResolutionError(Exception):
9 | pass
10 |
--------------------------------------------------------------------------------
/call_map/gui.py:
--------------------------------------------------------------------------------
1 | from .qt_compatibility import QtCore, QtGui, QtWidgets, Qt
2 | import re
3 | import threading
4 | import json
5 | import logging
6 | from typing import List, Tuple, Optional, Iterable
7 | from concurrent.futures import Executor, ThreadPoolExecutor, wait, Future
8 | from sys import modules as runtime_sys_modules, argv as sys_argv, platform as sys_platform, version_info as sys_version_info
9 |
10 | import toolz as tz
11 |
12 | import qtconsole.pygments_highlighter
13 | from pygments.lexers import PythonLexer
14 |
15 | from types import ModuleType
16 | from pathlib import Path
17 |
18 | #from . import wheel_fix
19 | from .core import OrganizerNode as ONode, Node
20 | from .config import get_user_config
21 | from .cache import read_text_cached
22 |
23 | from .core import UserScopeSettings, ScopeSettings, OrganizerNode, CodeElement
24 | from .errors import BadArgsError, ModuleResolutionError, ScriptResolutionError
25 | from .project_settings_module import Project
26 |
27 | from . import serialize
28 | from . import project_settings_module
29 |
30 | logger = logging.getLogger(__name__)
31 |
32 | COLUMN_WIDTH = 200
33 |
34 | def make_module(name):
35 | module = ModuleType(name)
36 | runtime_sys_modules[name] = module
37 | return module
38 |
39 | executors = make_module('call_map_executors')
40 | Debug = make_module('call_map_debug')
41 |
42 |
43 | def code_font():
44 | _code_font = QtGui.QFont('Monaco')
45 | _code_font.setStyleHint(QtGui.QFont.Monospace)
46 | _code_font.setPointSize(11)
47 | return _code_font
48 |
49 |
50 | def maybe_first(iterable):
51 | for ii in iterable:
52 | return ii
53 | else:
54 | return None
55 |
56 |
57 | def findParent(qobj, type_):
58 | while type(qobj) is not type_:
59 | qobj = qobj.parent()
60 |
61 | return qobj
62 |
63 |
64 | class CallListItem(QtWidgets.QListWidgetItem):
65 | default_role_markers = {'child': ' ', 'definition': '.', 'parent': '<', 'signature': '-'}
66 | role_markers = default_role_markers.copy()
67 | type_markers = {'module': 'm:', 'class': 'c:', 'script': 's:'}
68 | type_background_colors = {
69 | #'class': QtGui.QColor(240, 220, 240), # light purple
70 | #'module': QtGui.QColor(200, 240, 240), # light blue
71 | }
72 |
73 | type_foreground_colors = {
74 | 'class': QtGui.QColor(100, 20, 180), # dark purple
75 | 'module': QtGui.QColor(20, 100, 180), # dark blue
76 | 'script': QtGui.QColor(20, 100, 180), # dark blue
77 | }
78 |
79 | role_foreground_colors = {
80 | 'signature': QtGui.QColor('gray'), # gray
81 | }
82 |
83 | def __init__(self, node: Node):
84 | """The parents call the node, children are called by the node.
85 |
86 | If not callable, the Node has no children
87 |
88 | :param relation: relation to the CallListItem on the left
89 |
90 | """
91 | super().__init__()
92 |
93 | self.node = node
94 |
95 | name = self.node.code_element.name
96 |
97 | self.setFont(code_font())
98 |
99 | fontMetrics = QtGui.QFontMetrics(self.font())
100 |
101 | icon = self.role_markers[self.node.code_element.role]
102 |
103 | try:
104 | color = self.type_background_colors[node.code_element.type]
105 | except KeyError:
106 | pass
107 | else:
108 | self.setBackground(QtGui.QBrush(color))
109 |
110 | try:
111 | color = self.type_foreground_colors[node.code_element.type]
112 | except KeyError:
113 | pass
114 | else:
115 | self.setForeground(QtGui.QBrush(color))
116 |
117 | try:
118 | color = self.role_foreground_colors[self.node.code_element.role]
119 | except KeyError:
120 | pass
121 | else:
122 | self.setForeground(QtGui.QBrush(color))
123 |
124 | if isinstance(icon, str):
125 | if self.node.code_element.role == 'signature':
126 | fullText = icon + ' ' + '[sig]'
127 | else:
128 | fullText = icon + ' ' + self.type_markers.get(node.code_element.type, '') + name
129 | elidedText = fontMetrics.elidedText(fullText, Qt.Qt.ElideRight, COLUMN_WIDTH - 25)
130 | self.setText(elidedText)
131 | else:
132 | raise NotImplementedError
133 | self.setText(name)
134 | self.setIcon(icon)
135 |
136 | @classmethod
137 | def configure_role_markers(cls, want_unicode_role_markers):
138 | if want_unicode_role_markers:
139 | cls.role_markers = tz.merge(cls.default_role_markers, {'definition': '・', 'parent': '⊲'})
140 | else:
141 | cls.role_markers = cls.default_role_markers
142 |
143 | def walk_left(self):
144 | for ll in self.listWidget().walk_left():
145 | yield ll.currentItem()
146 |
147 | def walk_right(self):
148 | for ll in self.listWidget().walk_right():
149 | yield ll.currentItem()
150 |
151 | def showSource(self, path) -> Tuple[bool, bool, str]:
152 | # returns success, reuse, text
153 |
154 | if path:
155 | _path = Path(path)
156 |
157 | main_widget = findParent(self.listWidget(), MainWidget)
158 |
159 | text_edit = main_widget.text_edit
160 | if _path == text_edit.current_path:
161 | return (True, True, '')
162 |
163 | text = read_text_cached(_path)
164 |
165 | # see http://www.qtcentre.org/archive/index.php/t-52574.html
166 |
167 | text_edit.current_path = _path
168 |
169 | return (True, False, text)
170 | else:
171 | return (False, True, '')
172 |
173 | #line = node.code_element.start_pos[0] - 1
174 | #scrollToLine(text_edit, line - 8)
175 |
176 | def highlight(self, cancel_event):
177 | if cancel_event.is_set():
178 | return
179 |
180 | text_edit = findParent(self.listWidget(), MainWidget).text_edit
181 | sig = Signaler()
182 | sig.setPlainText_highlight_and_scroll.connect(text_edit.setPlainText_highlight_and_scroll)
183 |
184 | #if (self.node.code_element.role == 'definition'
185 | # and self.node.code_element.type == 'module'
186 | # and self.listWidget().index > 0):
187 | # text_edit.highlighter.reset()
188 | #else:
189 | success, reuse, text = self.showSource(self.node.code_element.call_pos[0])
190 |
191 | if cancel_event.is_set() or not success:
192 | text_edit.highlighter.reset()
193 | return
194 | else:
195 | call_pos = self.node.code_element.call_pos
196 | sig.setPlainText_highlight_and_scroll.emit(reuse, text, call_pos)
197 |
198 |
199 | def next_nodes(node: Node, cancel_event: threading.Event) -> List[Node]:
200 | if node.code_element.role == 'signature':
201 | return []
202 |
203 | if (type(node) != ONode
204 | and node.code_element.path != None
205 | and node.code_element.type != 'module'):
206 | signatures = [node.with_new_role('signature')]
207 | else:
208 | signatures = []
209 |
210 | if cancel_event.is_set(): return ()
211 |
212 | try:
213 | children = list(node.children)
214 | except Exception as exc:
215 | children = []
216 | logger.error('{}; while finding outbound connections of {}.'.format(exc, node), exc_info=get_user_config()['EXC_INFO'])
217 | if cancel_event.is_set(): return ()
218 |
219 | try:
220 | parents = list(node.parents)
221 | except Exception as exc:
222 | parents = []
223 | logger.error('{}; while finding inbound connections of {}.'.format(exc, node), exc_info=get_user_config()['EXC_INFO'])
224 | if cancel_event.is_set(): return ()
225 |
226 | return signatures + children + parents
227 |
228 |
229 | class UnthreadedExecutor:
230 | def submit(self, fn, *args, **kwargs) -> Future:
231 | result = fn(*args, **kwargs)
232 | future = Future()
233 | future.set_result(result)
234 | return future
235 |
236 |
237 | class MuxExecutor(Executor):
238 | def __init__(self, max_workers=None, thread_name_prefix=''):
239 | self.unthreaded_executor = UnthreadedExecutor()
240 | if sys_version_info >= (3, 6):
241 | self.thread_pool_executor = ThreadPoolExecutor(max_workers=max_workers,
242 | thread_name_prefix=thread_name_prefix)
243 | else:
244 | self.thread_pool_executor = ThreadPoolExecutor(max_workers=max_workers)
245 |
246 | def submit(self, fn, *args, **kwargs):
247 | if get_user_config()['MULTITHREADING']:
248 | executor = self.thread_pool_executor
249 | else:
250 | executor = self.unthreaded_executor
251 |
252 | return executor.submit(fn, *args, **kwargs)
253 |
254 |
255 | executors.highlight_executor = MuxExecutor(max_workers=1, thread_name_prefix='highlight')
256 | executors.main_executor = MuxExecutor(max_workers=1, thread_name_prefix='main')
257 |
258 |
259 | class Signaler(QtCore.QObject):
260 | """Handles Qt Signals for multithreading
261 |
262 | Qt requires that objects in different threads pass information only through
263 | signals. Various signals are gathered in the `Signaler` class to make passing
264 | arguments more like using plain functions.
265 |
266 | """
267 | add_next = QtCore.Signal(int, Node, list)
268 | insertItem = QtCore.Signal(int, CallListItem)
269 | insertCallListItem = QtCore.Signal(int, object)
270 | focus = QtCore.Signal(CallListItem)
271 | setPlainText_highlight_and_scroll = QtCore.Signal(bool, str, tuple)
272 | progress = QtCore.Signal(int)
273 | setNode = QtCore.Signal(Node, list)
274 |
275 |
276 | class CallList(QtWidgets.QListWidget):
277 |
278 | def __init__(self, map_widget, info_widget, index: int):
279 | super().__init__()
280 |
281 | self.map_widget = map_widget
282 | self.info_widget = info_widget
283 | self.index = index
284 |
285 | self.node = ONode('')
286 | self.nodes_to_items = {}
287 |
288 | self.currentItemChanged.connect(self.itemChangedSlot)
289 |
290 | self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
291 |
292 | # appearance
293 | self.focused_palette = QtGui.QPalette()
294 |
295 | self.unfocused_palette = QtGui.QPalette()
296 | self.unfocused_palette.setColor(QtGui.QPalette.HighlightedText,
297 | QtGui.QColor('black'))
298 | self.unfocused_palette.setColor(QtGui.QPalette.Highlight, QtGui.QColor('lightgray'))
299 |
300 | self.setPalette(self.unfocused_palette)
301 |
302 | self.setFocusPolicy(QtCore.Qt.StrongFocus)
303 |
304 | self.setMinimumWidth(COLUMN_WIDTH)
305 | self.setMaximumWidth(COLUMN_WIDTH)
306 |
307 | sizePolicy = QtWidgets.QSizePolicy()
308 | sizePolicy.setVerticalPolicy(Qt.QSizePolicy.Expanding)
309 |
310 | self.setSizePolicy(sizePolicy)
311 | self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
312 |
313 | #self.setAlternatingRowColors(True)
314 |
315 | self.populate_futures = []
316 | self.add_next_futures = []
317 | self.highlight_futures = []
318 |
319 | self.strict = False
320 |
321 | def setNode(self, node: Node, items: List[Node]):
322 | self.node = node
323 | self.clear()
324 | self.nodes_to_items.clear()
325 |
326 | for ii, item in enumerate(filter(tz.identity, items)): # filter null items -- TODO: find better place for filter
327 | self.insertCallListItem(ii, item)
328 |
329 | # NOTE: previously `setNode` involved submitting a job to an Executor
330 | # and appending the future to self.populate_futures. Currently
331 | # self.populate_futures is always empty.
332 |
333 | def focus(self, current):
334 | self.info_widget.showInfo(current.node)
335 | if not self.map_widget.auto_highlight:
336 | return
337 |
338 | text = ', '.join(('< ' if node.code_element.role == 'parent' else '') + node.code_element.name
339 | for node in self.map_widget.node_path())
340 |
341 | self.info_widget.setCallPath(text)
342 |
343 | cancel_event = threading.Event()
344 | fut = executors.highlight_executor.submit(current.highlight, cancel_event)
345 | fut.cancel_event = cancel_event
346 | self.highlight_futures.append(fut)
347 |
348 | def prepareToFocus(self):
349 | while self.highlight_futures:
350 | fut = self.highlight_futures.pop()
351 | fut.cancel()
352 | fut.cancel_event.set()
353 |
354 | def itemChangedSlot(self, current, previous):
355 | while self.add_next_futures:
356 | future = self.add_next_futures.pop()
357 | future.cancel()
358 | future.node.cancel_search()
359 |
360 | if not future.done() and not future.cancel_event.is_set():
361 | future.cancel_event.set()
362 |
363 | if current:
364 | try:
365 | node = current.node
366 | except AttributeError:
367 | return
368 |
369 | # clear immediately so user cannot issue commands on stale items
370 | self.map_widget.prepareToSetCallList(self.index + 1)
371 |
372 | self.prepareToFocus()
373 |
374 | wait(self.populate_futures + self.highlight_futures)
375 |
376 | self.focus(current)
377 |
378 | if self.strict:
379 | cancel_event = threading.Event()
380 | self.makeNextCallList(node, cancel_event)
381 | else:
382 | cancel_event = threading.Event()
383 | add_next_future = executors.main_executor.submit(self.makeNextCallList, node, cancel_event)
384 | add_next_future.node = node
385 | add_next_future.cancel_event = cancel_event
386 | self.add_next_futures.append(add_next_future)
387 |
388 | def _showCallPath(self):
389 | path = list(tz.cons(self.currentItem().node, (ll.currentItem().node for ll in self.walk_left())))
390 | path.reverse()
391 |
392 | text = ', '.join(('< ' if node.code_element.role == 'parent' else '')
393 | + node.code_element.name
394 | for node in path)
395 |
396 | self.info_widget.setCallPath(text)
397 |
398 | def remove_nodes(self, nodes: Iterable[Node]):
399 | for node in nodes:
400 | item = self.nodes_to_items.pop(node)
401 | self.takeItem(self.row(item))
402 | self.removeItemWidget(item)
403 | #assert 0
404 | #print(self.items())
405 |
406 | def add_nodes(self, nodes: Iterable[Node]):
407 | start = self.count()
408 | for ii, node in enumerate(nodes):
409 | self.insertCallListItem(start + ii, node)
410 |
411 | @QtCore.Slot(int, object)
412 | def insertCallListItem(self, ii, node: Node):
413 | self.nodes_to_items[node] = CallListItem(node)
414 | self.insertItem(ii, CallListItem(node))
415 |
416 | def makeNextCallList(self, node: Node, cancel_event: threading.Event):
417 | items = next_nodes(node, cancel_event)
418 | if not cancel_event.is_set():
419 | next_call_list = self.map_widget.callLists[self.index + 1]
420 | if self.strict:
421 | next_call_list.strict = True
422 | next_call_list.setNode(node, items)
423 | next_call_list.strict = False
424 | else:
425 | signaler = Signaler()
426 | signaler.setNode.connect(next_call_list.setNode)
427 | signaler.setNode.emit(node, items)
428 |
429 | def walk_right(self):
430 | return self.map_widget.callLists[self.index + 1:]
431 |
432 | def walk_left(self):
433 | if self.index > 0:
434 | return self.map_widget.callLists[self.index - 1: :-1]
435 | else:
436 | return ()
437 |
438 | def next_call_list(self):
439 | try:
440 | return self.map_widget.callLists[self.index + 1]
441 | except IndexError:
442 | return None
443 |
444 | def prev_call_list(self):
445 | if self.index > 0:
446 | return self.map_widget.callLists[self.index - 1]
447 | else:
448 | return None
449 |
450 | def focusInEvent(self, event):
451 | current = self.currentItem()
452 | if current:
453 | self.prepareToFocus()
454 | wait(self.populate_futures + self.highlight_futures)
455 | self.focus(current)
456 | self.setPalette(self.focused_palette)
457 |
458 | def focusOutEvent(self, event):
459 | if event.reason() != QtCore.Qt.ActiveWindowFocusReason:
460 | self.setPalette(self.unfocused_palette)
461 |
462 | def keyPressEvent(self, event):
463 | super().keyPressEvent(event) #ll.setFocus()
464 |
465 | if not event.isAccepted():
466 | if event.key() == Qt.Qt.Key_Right:
467 | _next = self.next_call_list()
468 | if _next and _next.count() > 0:
469 | if all(future.done() for future in
470 | tz.concatv(self.populate_futures, self.add_next_futures)):
471 | _next.setFocus()
472 | self.map_widget.ensureWidgetVisible(_next, 0, 0)
473 | if self.next_call_list().currentItem() is None:
474 | self.next_call_list().setCurrentRow(0)
475 | else:
476 | wait_item = _next.item(0)
477 | wait_item.poked += 1
478 | if wait_item.poked == 3:
479 | wait_item.setText('QUIT POKING ME')
480 | event.accept()
481 |
482 | if event.key() == Qt.Qt.Key_Left:
483 | _prev = self.prev_call_list()
484 | if _prev:
485 | _prev.setFocus()
486 | (self.map_widget.ensureWidgetVisible(_prev, 0, 0))
487 | event.accept()
488 |
489 | if event.key() == Qt.Qt.Key_Space:
490 | pass
491 |
492 | class MapLayout(QtWidgets.QHBoxLayout):
493 | def __init__(self, parent):
494 | super().__init__(parent)
495 | #self.addStretch(1)
496 |
497 | def sizeHint(self):
498 | size = super().sizeHint()
499 | width = self.count() * COLUMN_WIDTH
500 | return QtCore.QSize(width, self.parent().maximumHeight())
501 |
502 | def maximumSize(self):
503 | width = self.count() * COLUMN_WIDTH
504 | return QtCore.QSize(width, super().maximumSize().height())
505 |
506 | def minimumSize(self):
507 | width = self.count() * COLUMN_WIDTH
508 | return QtCore.QSize(width, super().minimumSize().height())
509 |
510 |
511 | class MapWidget(QtWidgets.QScrollArea):
512 | def __init__(self, parent, info_widget, status_bar, node: Node):
513 | super().__init__(parent)
514 |
515 | self.info_widget = info_widget
516 | self.status_bar = status_bar
517 |
518 | self.callLists = []
519 |
520 | self.setFocusPolicy(QtCore.Qt.StrongFocus)
521 | self.setWidget(QtWidgets.QWidget(self))
522 |
523 | layout = MapLayout(self.widget())
524 | #layout.setAlignment(QtCore.Qt.AlignRight)
525 | layout.setContentsMargins(0,0,0,0)
526 | layout.setSpacing(0)
527 |
528 | layout.setSizeConstraint(Qt.QLayout.SetMinAndMaxSize)
529 |
530 | self.currentIndex = -1
531 | self.prepareToSetCallList(0)
532 |
533 | self.root_list_items = {} # type: Dict[Node, CallListItem]
534 |
535 | self.callLists[0].setNode(node, node.children)
536 | wait(self.callLists[0].populate_futures)
537 |
538 | for ii in range(self.callLists[0].count()):
539 | item = self.callLists[0].item(ii)
540 | child_node = item.node
541 | self.root_list_items[child_node] = item
542 |
543 | self.auto_highlight = True
544 |
545 | def prepareToSetCallList(self, index):
546 | oldIndex = self.currentIndex
547 | self.currentIndex = index
548 |
549 | try:
550 | ll = self.callLists[index]
551 | except IndexError:
552 | ll = CallList(self, self.info_widget, len(self.callLists))
553 | self.callLists.append(ll)
554 |
555 | self.widget().layout().insertWidget(ll.index, ll)
556 |
557 | while ll.populate_futures:
558 | fut = ll.populate_futures.pop()
559 | fut.cancel()
560 | fut.cancel_event.set()
561 |
562 | ll.clear()
563 | ll.nodes_to_items.clear()
564 | item = QtWidgets.QListWidgetItem('wait . . .')
565 | item.setFlags(QtCore.Qt.ItemNeverHasChildren)
566 | item.poked = 0
567 | ll.addItem(item)
568 | ll.show()
569 | self.ensureWidgetVisible(ll)
570 |
571 | if oldIndex != index:
572 | for ll in self.callLists[index+1:]:
573 | ll.hide()
574 | self.widget().layout().removeWidget(ll)
575 |
576 | def resizeEvent(self, event):
577 | # Note: must do super().resizeEvent before resizing the self.widget,
578 | # otherwise there will be minor graphical glitches.
579 | super().resizeEvent(event)
580 | self.widget().resize(self.widget().size().width(), event.size().height())
581 |
582 | def node_path(self):
583 | for ll in self.callLists:
584 | if ll.currentItem():
585 | yield ll.currentItem().node
586 | else:
587 | break
588 |
589 | def open_bookmark(self, bookmark_code_element_path: Iterable[CodeElement]):
590 | def same_except_location(aa: CodeElement, bb: CodeElement):
591 | return (aa.name == bb.name and
592 | aa.module == bb.module and
593 | aa.type == bb.type and
594 | aa.role == bb.role)
595 |
596 | def same_except_name(aa: CodeElement, bb: CodeElement):
597 | return (aa.module == bb.module and
598 | aa.type == bb.type and
599 | aa.role == bb.role and
600 | aa.call_pos == bb.call_pos)
601 |
602 | for ii, bookmark_code_element in enumerate(bookmark_code_element_path):
603 | ll = self.callLists[ii] # type: CallList
604 |
605 | # TODO: use a with block to set strictness
606 | ll.strict = True
607 |
608 | wait(ll.populate_futures + ll.highlight_futures + ll.add_next_futures)
609 |
610 | near_misses = []
611 |
612 | for jj in range(ll.count()):
613 | ll_item = ll.item(jj)
614 | ll_code_element = ll_item.node.code_element
615 | if ll_code_element == bookmark_code_element:
616 | ll.setCurrentItem(ll_item)
617 | ll.setFocus()
618 | wait(ll.add_next_futures)
619 | break
620 | elif same_except_location(ll_code_element, bookmark_code_element):
621 | near_misses.append((jj, 'Same except location.'))
622 |
623 | elif same_except_name(ll_code_element, bookmark_code_element):
624 | near_misses.append((jj, 'Same except name.'))
625 | else:
626 | if near_misses:
627 | idx, note = near_misses[0]
628 | self.status_bar.showMessage(
629 | 'Exact bookmark not found. Nearest match followed. ({})'.format(note),
630 | msecs=10000)
631 | ll.setCurrentRow(idx)
632 | ll.setFocus()
633 | else:
634 | ll.setCurrentRow(0)
635 | ll.setFocus()
636 | ll.strict = False
637 | return
638 |
639 | ll.strict = False
640 |
641 | def toggle_auto_highlight(self):
642 | self.auto_highlight = not self.auto_highlight
643 | self.status_bar.showMessage(
644 | 'Auto highlight toggled {}'.format(
645 | 'on' if self.auto_highlight else 'off'), msecs=3000)
646 |
647 | self.status_bar.update()
648 |
649 | if self.auto_highlight:
650 | current_callList = tz.first(cl for cl in self.callLists if cl.hasFocus())
651 | current = current_callList.currentItem()
652 | if current:
653 | current_callList.focus(current)
654 |
655 |
656 | class InfoWidget(QtWidgets.QWidget):
657 | """Displays information about a node"""
658 | labels = ['code_path', 'name', 'type', 'module', 'position', 'defined at']
659 | auto_labels = ['name', 'type', 'module']
660 |
661 | def __init__(self, status_bar, parent=None):
662 | super().__init__(parent)
663 |
664 | self.status_bar = status_bar
665 | self.setLayout(QtWidgets.QGridLayout(self))
666 | self.current_code_element = None
667 |
668 | self.label_to_row = {label: ii for ii, label in enumerate(self.labels)}
669 |
670 | for ii, label in enumerate(self.labels):
671 | lineEdit = QtWidgets.QLineEdit(self)
672 | lineEdit.setFont(code_font())
673 | lineEdit.setReadOnly(True)
674 | qlabel = QtWidgets.QLabel('&' + label + ':', self)
675 | qlabel.setBuddy(lineEdit)
676 | qlabel.setAlignment(Qt.Qt.AlignLeft | Qt.Qt.AlignVCenter)
677 |
678 | self.layout().addWidget(qlabel, ii, 0)
679 | self.layout().addWidget(lineEdit, ii, 1)
680 |
681 | open_position_button = QtWidgets.QPushButton('Open')
682 | open_def_button = QtWidgets.QPushButton('Open')
683 |
684 | self.layout().addWidget(open_position_button, self.label_to_row['position'], 2)
685 | self.layout().addWidget(open_def_button, self.label_to_row['defined at'], 2)
686 |
687 |
688 | @open_position_button.clicked.connect
689 | def onclick():
690 | if self.current_code_element:
691 | self.open_in_editor_with_user_defined_function(
692 | self.current_code_element.call_pos[0],
693 | self.current_code_element.call_pos[1][0])
694 |
695 |
696 | @open_def_button.clicked.connect
697 | def onclick():
698 | if self.current_code_element and self.current_code_element.path:
699 | self.open_in_editor_with_user_defined_function(
700 | self.current_code_element.path,
701 | self.current_code_element.start_pos[0])
702 |
703 |
704 | self.layout().addItem(QtWidgets.QSpacerItem(0,0))
705 | self.layout().setRowStretch(self.layout().rowCount(), 1)
706 |
707 |
708 | def showInfo(self, node: Node):
709 | try:
710 | code_element = node.code_element
711 | except AttributeError:
712 | pass
713 | else:
714 | Debug.code_element = code_element
715 | Debug.node = node
716 |
717 | self.current_code_element = code_element
718 |
719 | for ii, label in enumerate(self.labels):
720 | lineEdit = self.layout().itemAtPosition(ii, 1).widget()
721 |
722 | if label == 'code_path':
723 | continue
724 | elif label in self.auto_labels:
725 | value = getattr(code_element, label)
726 | elif label == 'defined at':
727 | if code_element.start_pos:
728 | value = '{} : {}'.format(code_element.path, code_element.start_pos[0])
729 | elif code_element.path:
730 | value = code_element.path
731 | else:
732 | value = '[Unknown]'
733 | elif label == 'position':
734 | value = '{} : {}'.format(code_element.call_pos[0], code_element.call_pos[1][0])
735 |
736 | lineEdit.setText(value if isinstance(value, str)
737 | else repr(value))
738 |
739 | def setCallPath(self, call_path):
740 | lineEdit = self.layout().itemAtPosition(0, 1).widget()
741 | lineEdit.setText(call_path)
742 |
743 | def open_in_editor_with_user_defined_function(self, path, line):
744 | user_defined_function = (get_user_config()['open_in_editor'])
745 | try:
746 | user_defined_function(path, line)
747 | except:
748 | message = 'Cannot open {} at line {}'.format(path, line)
749 | logger.exception(message)
750 | self.status_bar.showMessage(message, 10000)
751 |
752 |
753 | class TargetHighlighter:
754 | def __init__(self, text_edit):
755 | self.text_edit = text_edit
756 | self.current_highlight_cursor = None
757 |
758 | def highlight(self, call_pos):
759 | self.reset()
760 |
761 | if call_pos[0] != None and call_pos[1] != (None, None):
762 | document = self.text_edit.document()
763 | self.call_pos = call_pos
764 | cursor = QtGui.QTextCursor(
765 | document.findBlockByLineNumber(call_pos[1][0] - 1))
766 | cursor.setPosition(cursor.block().position() + call_pos[1][1])
767 |
768 | anchor = cursor.position()
769 | cursor.movePosition(QtGui.QTextCursor.NextWord, anchor)
770 |
771 | self.current_highlight_cursor = cursor
772 | cursor.orig_format = cursor.charFormat()
773 |
774 | # WARNING: To be safe, make a new QTextCharFormat every time. If you
775 | # use the same one for more than one document, it will cause many
776 | # glitches. I do not know exactly when it is safe to share a format.
777 | # -- Andy Lee
778 | _fmt = QtGui.QTextCharFormat()
779 | _fmt.setBackground(QtGui.QColor('lightskyblue'))
780 | cursor.setCharFormat(_fmt)
781 |
782 | def reset(self):
783 | cc = self.current_highlight_cursor
784 | if cc:
785 | cc.setCharFormat(cc.orig_format)
786 | self.current_highlight_cursor = None
787 |
788 |
789 | class PlainTextEdit(QtWidgets.QTextEdit):
790 | def setPlainText_highlight_and_scroll(self, reuse: bool, text: str, call_pos: tuple):
791 | if not reuse:
792 | self.setPlainText(text)
793 |
794 | self.highlighter.highlight(call_pos)
795 | if call_pos[1][0]:
796 | self.scrollToLine(call_pos[1][0] - 1 - 8)
797 |
798 | def scrollToLine(self, line):
799 | cursor = self.textCursor()
800 | block = self.document().findBlockByLineNumber(line)
801 | cursor.setPosition(block.position())
802 |
803 | sb = self.verticalScrollBar()
804 | sb.setValue(sb.maximum())
805 |
806 | self.setTextCursor(cursor)
807 |
808 | #Widgets.text_edit.scrollBar().scrollContentsBy(0, 10)
809 | #Widgets.text_edit.horizontalScrollBar().setValue(10,10)
810 |
811 |
812 | class SettingsWidget(QtWidgets.QTabWidget):
813 | def __init__(self, project: Project, map_widget: MapWidget, status_bar: QtWidgets.QStatusBar, parent=None):
814 | super().__init__(parent)
815 |
816 | self.project = project
817 | self.map_widget = map_widget
818 | self.status_bar = status_bar
819 |
820 | self.path_settings_widget = TextSettingsWidget(
821 | project, map_widget, status_bar, project_settings_module.sys_path, parent=self)
822 |
823 | self.module_settings_widget = TextSettingsWidget(
824 | project, map_widget, status_bar, project_settings_module.modules, parent=self)
825 |
826 | self.script_settings_widget = TextSettingsWidget(
827 | project, map_widget, status_bar, project_settings_module.scripts, parent=self)
828 |
829 | self.project_settings_widget = TextSettingsWidget(
830 | project, map_widget, status_bar, project_settings_module.project_settings, parent=self)
831 |
832 | self.bookmarks_json_widget = TextSettingsWidget(
833 | project, map_widget, status_bar, project_settings_module.bookmarks, parent=self)
834 |
835 | self.bookmarks_widget = BookmarksWidget(project, map_widget, parent=self)
836 |
837 | self.bookmarks_widget.bookmarks_changed.connect(self.bookmarks_json_widget.load_project_settings)
838 | self.bookmarks_json_widget.settings_changed.connect(self.bookmarks_widget.load_from_project_settings)
839 |
840 | self.addTab(self.bookmarks_widget, "bookmarks")
841 | self.addTab(self.path_settings_widget, "sys path")
842 | self.addTab(self.module_settings_widget, "modules")
843 | self.addTab(self.script_settings_widget, "scripts")
844 | self.addTab(self.project_settings_widget, "project settings")
845 | self.addTab(self.bookmarks_json_widget, "bookmarks (json)")
846 |
847 | if sys_platform == 'darwin':
848 | self.setStyleSheet('''
849 | QTabWidget::tab-bar {
850 | left: 3px;
851 | }
852 | ''')
853 |
854 | #self.minimumSize = QtCore.QSize(200, 200)
855 |
856 |
857 | class TextSettingsWidget(QtWidgets.QWidget):
858 | settings_changed = QtCore.Signal(list, list)
859 |
860 | def __init__(self, project: Project, map_widget: MapWidget, status_bar: QtWidgets.QStatusBar,
861 | category: str, parent=None):
862 | super().__init__(parent)
863 |
864 | self.__init_gui_elements__()
865 |
866 | self.project = project
867 | self.map_widget = map_widget
868 | self.status_bar = status_bar
869 | self.category = category
870 | self.load_project_settings()
871 | self.textEdit.textChanged.connect(self.enableButtons)
872 |
873 | def __init_gui_elements__(self):
874 | self.setLayout(QtWidgets.QVBoxLayout(self))
875 | self.layout().setContentsMargins(3,3,3,3)
876 | self.layout().setSpacing(3)
877 |
878 | self.textEdit = QtWidgets.QTextEdit()
879 | self.textEdit.setFont(code_font())
880 | self.saveButton = QtWidgets.QPushButton('Commit')
881 | self.cancelButton = QtWidgets.QPushButton('Cancel')
882 |
883 | #self.buttonGroup = QtWidgets.QButtonGroup(self)
884 | #self.buttonGroup.addButton(self.saveButton)
885 | #self.buttonGroup.addButton(self.cancelButton)
886 | self.buttonLayout = QtWidgets.QHBoxLayout()
887 | self.buttonLayout.addStretch(-1)
888 | self.buttonLayout.setContentsMargins(1,1,1,1)
889 | self.buttonLayout.setSpacing(15)
890 |
891 | self.saveButton.setMaximumWidth(200)
892 | self.saveButton.setMinimumWidth(100)
893 | self.cancelButton.setMaximumWidth(200)
894 | self.cancelButton.setMinimumWidth(100)
895 |
896 | self.layout().addWidget(self.textEdit)
897 | self.layout().addLayout(self.buttonLayout)
898 |
899 | self.buttonLayout.addWidget(self.saveButton)
900 | self.buttonLayout.addWidget(self.cancelButton)
901 |
902 | self.saveButton.pressed.connect(self.commit)
903 | self.cancelButton.pressed.connect(self.load_project_settings)
904 |
905 | def enableButtons(self):
906 | self.saveButton.setEnabled(True)
907 | self.cancelButton.setEnabled(True)
908 |
909 | def load_project_settings(self):
910 | text = json.dumps(self.project.encode(self.category, for_persistence=False), indent=True, sort_keys=True)
911 | self.textEdit.setPlainText(text)
912 | self.saveButton.setDisabled(True)
913 | self.cancelButton.setDisabled(True)
914 |
915 | def commit(self):
916 | type_ = project_settings_module.category_type[self.category]
917 | try:
918 | decoded = serialize.decode(type_, json.loads(self.textEdit.toPlainText()))
919 | except json.JSONDecodeError as err:
920 | message = 'Cannot decode JSON; {}'.format(err.args[0])
921 | logger.error(message)
922 | self.status_bar.showMessage(message, 10000)
923 | self.saveButton.setDisabled(True)
924 | return
925 | except serialize.DecodeError as err:
926 | message = '{}'.format(err.args[0])
927 | logger.error(message)
928 | self.status_bar.showMessage(message, 10000)
929 | self.saveButton.setDisabled(True)
930 | return
931 |
932 | try:
933 | has_changed, stale, additional = self.project.set_settings(self.category, decoded)
934 | except (ValueError, TypeError) as err:
935 | # TypeError -- raised by initial type validation in `set_settings`.
936 | # ModuleResolutionError -- raised by `module_nodes`
937 | # ScriptResolutionError -- raised by `script_nodes`
938 |
939 | message = 'Invalid settings; {}: {}'.format(type(err).__name__, err.args[0])
940 | logger.error(message)
941 | self.status_bar.showMessage(message, 10000)
942 | self.saveButton.setDisabled(True)
943 | return
944 |
945 | status_messages = []
946 | for key, err in tz.concatv(self.project.failures['python'][project_settings_module.modules].items(),
947 | self.project.failures['python'][project_settings_module.scripts].items()):
948 | message = '{}: {}'.format(type(err).__name__, err.args[0])
949 | logger.error(message)
950 | status_messages.append((logging.ERROR, message))
951 |
952 | if has_changed:
953 | if self.category in (project_settings_module.modules,
954 | project_settings_module.scripts):
955 | self.map_widget.callLists[0].remove_nodes(stale)
956 | self.map_widget.callLists[0].add_nodes(additional)
957 |
958 | # TODO: add/remove stale nodes in other callLists
959 |
960 | elif self.category == project_settings_module.files:
961 | print('TODO: implement settings update for {}'.format(self.category))
962 | elif self.category == project_settings_module.sys_path:
963 | # 1. Record code path
964 | # 2. regenerate root nodes and root list
965 | # 3. Follow previous code path
966 |
967 | old_code_element_path = list(node.code_element for node in self.map_widget.node_path())
968 |
969 | node = OrganizerNode('Root', [],
970 | list(tz.concatv(self.project.module_nodes['python'].values(),
971 | self.project.script_nodes['python'].values())))
972 |
973 | try:
974 | children = node.children
975 | except Exception as exc:
976 | children = []
977 | logger.error('{}; while finding inbound connections of {}.'.format(exc, node),
978 | exc_info=get_user_config()['EXC_INFO'])
979 |
980 | self.map_widget.callLists[0].setNode(node, children)
981 |
982 | self.map_widget.callLists[0].setFocus()
983 |
984 | self.map_widget.open_bookmark(old_code_element_path)
985 |
986 | elif self.category == project_settings_module.bookmarks:
987 | self.settings_changed.emit(stale, additional)
988 |
989 | elif self.category == 'project_settings':
990 | self.settings_changed.emit(stale, additional)
991 |
992 | else:
993 | print('TODO: implement settings update for {}'.format(self.category))
994 |
995 | try:
996 | self.project.update_persistent_storage()
997 | except Exception as err:
998 | message = 'Cannot save settings; {}'.format(err.args[0])
999 | logger.error(err, exc_info=get_user_config()['EXC_INFO'])
1000 | status_messages.append((logging.ERROR, err.args[0]))
1001 | self.load_project_settings() # reload to reformat text
1002 | else:
1003 | status_messages.append((logging.INFO, 'Saved.'))
1004 | self.load_project_settings() # reload to reformat text
1005 |
1006 | else:
1007 | status_messages.append((logging.INFO, 'Not saved (no change).'))
1008 | self.load_project_settings() # reload to reformat text
1009 |
1010 | self.status_bar.showMessage('. '.join(message for level, message in status_messages), 10000)
1011 |
1012 |
1013 | def bookmark_to_str(bookmark):
1014 | return ', '.join(('< ' if code_element.role == 'parent' else '') + code_element.name
1015 | for code_element in bookmark)
1016 |
1017 |
1018 | class BookmarksWidget(QtWidgets.QWidget):
1019 | bookmarks_changed = QtCore.Signal()
1020 |
1021 | def __init__(self, project: Project, map_widget: MapWidget, parent=None):
1022 | super().__init__(parent)
1023 | self.project = project
1024 | self.map_widget = map_widget
1025 |
1026 | self.setLayout(QtWidgets.QVBoxLayout(self))
1027 | self.layout().setContentsMargins(3,3,3,3)
1028 | self.layout().setSpacing(3)
1029 |
1030 | self.buttonLayout = QtWidgets.QHBoxLayout()
1031 | self.buttonLayout.setContentsMargins(1,1,1,1)
1032 | self.buttonLayout.setSpacing(4)
1033 | self.buttonLayout.addStretch(-1)
1034 |
1035 | self.visitButton = QtWidgets.QPushButton('Visit')
1036 | self.visitButton.setMaximumWidth(200)
1037 | self.visitButton.setMinimumWidth(100)
1038 | self.addButton = QtWidgets.QPushButton('Add')
1039 | self.addButton.setMaximumWidth(200)
1040 | self.addButton.setMinimumWidth(100)
1041 | self.deleteButton = QtWidgets.QPushButton('Delete')
1042 | self.deleteButton.setMaximumWidth(200)
1043 | self.deleteButton.setMinimumWidth(100)
1044 | self.buttonLayout.addWidget(self.visitButton)
1045 | self.buttonLayout.addWidget(self.addButton)
1046 | self.buttonLayout.addWidget(self.deleteButton)
1047 | self.buttonLayout.addStretch(-1)
1048 |
1049 | self.visitButton.pressed.connect(self.visitBookmark)
1050 | self.addButton.pressed.connect(self.addBookmark)
1051 | self.deleteButton.pressed.connect(self.deleteBookmark)
1052 |
1053 | self.listWidget = QtWidgets.QListWidget()
1054 |
1055 | self.layout().addWidget(self.listWidget)
1056 | self.layout().addLayout(self.buttonLayout)
1057 |
1058 | self.load_from_project_settings()
1059 |
1060 | def load_from_project_settings(self):
1061 | self.listWidget.clear()
1062 |
1063 | for ii, bookmark in enumerate(self.project.settings[project_settings_module.bookmarks]):
1064 | item = QtWidgets.QListWidgetItem(bookmark_to_str(bookmark))
1065 | item.setFont(code_font())
1066 | item.setData(1, bookmark)
1067 | self.listWidget.insertItem(ii, item)
1068 |
1069 | def visitBookmark(self):
1070 | item = self.listWidget.currentItem()
1071 | if item:
1072 | bookmark = item.data(1)
1073 | self.map_widget.open_bookmark(bookmark)
1074 |
1075 | def addBookmark(self):
1076 | bookmark = list(node.code_element for node in self.map_widget.node_path())
1077 | if bookmark:
1078 | self.project.settings[project_settings_module.bookmarks].append(bookmark)
1079 | item = QtWidgets.QListWidgetItem(bookmark_to_str(bookmark))
1080 | item.setFont(code_font())
1081 | item.setData(1, bookmark)
1082 | self.listWidget.addItem(item)
1083 | self.project.update_persistent_storage()
1084 |
1085 | self.bookmarks_changed.emit()
1086 | else:
1087 | logger.info('Empty bookmark ignored.')
1088 |
1089 | def deleteBookmark(self):
1090 | item = self.listWidget.currentItem()
1091 | if item:
1092 | bookmark = item.data(1)
1093 | self.listWidget.removeItemWidget(item)
1094 | self.project.settings[project_settings_module.bookmarks].remove(bookmark)
1095 |
1096 | self.project.update_persistent_storage()
1097 | self.load_from_project_settings()
1098 |
1099 | self.bookmarks_changed.emit()
1100 |
1101 |
1102 | def make_test_node():
1103 | test_node = ONode('AAA',
1104 | parents=[ONode('BBB'), ONode('CCC')],
1105 | children=[ONode('DDD'), ONode('EEE')])
1106 |
1107 | for ii in range(100):
1108 | test_node.children.append(ONode('X_' + repr(ii)))
1109 |
1110 | for child in test_node.children:
1111 | child.children.append(ONode('Y' + child.code_element.name, [],
1112 | [ONode('Z1' + child.code_element.name),
1113 | ONode('Z2' + child.code_element.name)]))
1114 |
1115 | root_node = ONode('Root', [], [test_node])
1116 |
1117 | return root_node
1118 |
1119 |
1120 | class MyQApplication(QtWidgets.QApplication):
1121 |
1122 | def legacy_notify(self, obj: QtCore.QObject, event: QtCore.QEvent):
1123 | """Fixes scrolling weirdness
1124 |
1125 | This is a legacy method for `notify` from before PyQt5. It doesn't work
1126 | with PyQt5.
1127 |
1128 | """
1129 | if (event.type() == QtCore.QEvent.Wheel):
1130 | return wheel_fix.notify_wheel_event(self, obj, event)
1131 | else:
1132 | return super().notify(obj, event)
1133 |
1134 |
1135 | class MainWidget(QtWidgets.QSplitter):
1136 | def __init__(self, parent=None):
1137 | super().__init__(parent)
1138 |
1139 | self.setOrientation(QtCore.Qt.Horizontal)
1140 | self.left_widget = QtWidgets.QWidget(self)
1141 | self.right_widget = QtWidgets.QWidget(self)
1142 | self.right_splitter = QtWidgets.QSplitter(self.right_widget)
1143 | self.right_splitter.setOrientation(QtCore.Qt.Vertical)
1144 |
1145 | self.left_layout = QtWidgets.QVBoxLayout(self.left_widget)
1146 | self.right_layout = QtWidgets.QVBoxLayout(self.right_widget)
1147 |
1148 | self.addWidget(self.left_widget)
1149 | self.addWidget(self.right_widget)
1150 | self.right_layout.addWidget(self.right_splitter)
1151 |
1152 | self.left_layout.setContentsMargins(0,0,0,0)
1153 | self.right_layout.setContentsMargins(0,0,0,0)
1154 | self.right_splitter.setContentsMargins(0,0,0,0)
1155 |
1156 | self.left_layout.setSpacing(0.0)
1157 | self.right_layout.setSpacing(0.0)
1158 |
1159 |
1160 | def finalize(self):
1161 | self.setStretchFactor(0, 2)
1162 | self.setStretchFactor(1, 1)
1163 | self.right_splitter.setStretchFactor(0, 2)
1164 | self.right_splitter.setStretchFactor(1, 1)
1165 |
1166 | self.setHandleWidth(1)
1167 | self.setCollapsible(0, False)
1168 | self.setCollapsible(1, False)
1169 |
1170 | self.right_splitter.setCollapsible(0, False)
1171 | self.right_splitter.setCollapsible(1, True)
1172 |
1173 |
1174 | def make_app(user_scope_settings: UserScopeSettings, project_directory: Optional[str],
1175 | enable_ipython_support: bool = False, show_gui: bool = True):
1176 | from .jedi_dump import make_scope_settings
1177 |
1178 | ui_toplevel = ModuleType('call_map_ui_toplevel')
1179 |
1180 | project = project_settings_module.Project(project_directory)
1181 | ui_toplevel.project = project
1182 |
1183 | stored_settings = project.load_from_persistent_storage()
1184 | is_new_project = bool(stored_settings)
1185 | project.update_settings(stored_settings)
1186 |
1187 | scope_settings = make_scope_settings(is_new_project, project.scope_settings, user_scope_settings)
1188 |
1189 | project.settings.update(
1190 | {project_settings_module.modules: scope_settings.module_names,
1191 | project_settings_module.scripts: scope_settings.scripts,
1192 | project_settings_module.sys_path: scope_settings.effective_sys_path})
1193 |
1194 | project.update_module_resolution_path('python')
1195 | project.make_platform_specific_nodes('python')
1196 |
1197 | errors = list(tz.concatv(project.failures['python'][project_settings_module.modules].values(),
1198 | project.failures['python'][project_settings_module.scripts].values()))
1199 | # will also put in status bar later
1200 | for error in errors:
1201 | logger.error(error)
1202 |
1203 | try:
1204 | ui_toplevel.project.update_persistent_storage()
1205 | except FileNotFoundError as err:
1206 | raise BadArgsError(err)
1207 |
1208 | node = OrganizerNode('Root', [],
1209 | list(tz.concatv(project.module_nodes['python'].values(),
1210 | project.script_nodes['python'].values())))
1211 |
1212 | CallListItem.configure_role_markers(
1213 | get_user_config()['UNICODE_ROLE_MARKERS'])
1214 |
1215 | app = QtCore.QCoreApplication.instance()
1216 | if app is None:
1217 | app = MyQApplication(sys_argv)
1218 | ui_toplevel.app = app
1219 |
1220 |
1221 | app.setQuitOnLastWindowClosed(True)
1222 |
1223 | main_window = QtWidgets.QMainWindow()
1224 | main_window.layout().setSpacing(0)
1225 | main_window.layout().setContentsMargins(0, 0, 0, 0)
1226 |
1227 | main_widget = MainWidget(main_window)
1228 | main_window.setCentralWidget(main_widget)
1229 |
1230 | status_bar = main_window.statusBar()
1231 | ui_toplevel.status_bar = status_bar
1232 | status_bar.setSizeGripEnabled(False)
1233 |
1234 | info_widget = InfoWidget(main_widget, status_bar)
1235 | ui_toplevel.info_widget = info_widget
1236 |
1237 | map_widget = MapWidget(main_widget, info_widget, status_bar, node)
1238 | ui_toplevel.map_widget = map_widget
1239 |
1240 | text_edit_0 = PlainTextEdit()
1241 | text_edit_0.current_path = None
1242 | text_edit_0.highlighter = TargetHighlighter(text_edit_0)
1243 |
1244 | text_edit_0.setReadOnly(True)
1245 | text_edit_0.setFont(code_font())
1246 |
1247 | ui_toplevel.pygments_highlighter = qtconsole.pygments_highlighter.PygmentsHighlighter(
1248 | text_edit_0.document(), lexer=PythonLexer())
1249 |
1250 |
1251 | text_edit_1 = QtWidgets.QTextEdit()
1252 | text_edit_1.setFont(code_font())
1253 |
1254 | main_widget.text_edit = text_edit_0
1255 |
1256 |
1257 | toggle_action = QtWidgets.QAction('&Turn Auto Highlight Off', main_window)
1258 | view_menu = main_window.menuBar().addMenu('&View')
1259 | view_menu.addAction(toggle_action)
1260 |
1261 | def _update_auto_highlight_menu_text():
1262 | action_on_or_off = 'Off' if map_widget.auto_highlight else 'On'
1263 | toggle_action.setText('&Turn Auto Highlight {}'.format(action_on_or_off))
1264 |
1265 | toggle_action.triggered.connect(map_widget.toggle_auto_highlight)
1266 | toggle_action.triggered.connect(_update_auto_highlight_menu_text)
1267 |
1268 | ui_toplevel.settings_widget = SettingsWidget(ui_toplevel.project, map_widget, status_bar)
1269 |
1270 | def configure_sizes():
1271 | main_widget.setSizes([4, 3])
1272 |
1273 | main_widget.right_splitter.setSizes([2,1])
1274 |
1275 | map_widget.setMinimumWidth(500)
1276 | map_widget.setSizePolicy(QtWidgets.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding))
1277 |
1278 | info_widget.setSizePolicy(QtWidgets.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Maximum))
1279 |
1280 | text_edit_0.setMinimumWidth(400)
1281 | text_edit_0.setMinimumHeight(300)
1282 | text_edit_0.setSizePolicy(QtWidgets.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding))
1283 |
1284 | settings_widget = ui_toplevel.settings_widget
1285 | settings_widget.setMinimumWidth(400)
1286 | settings_widget.setSizePolicy(QtWidgets.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Minimum))
1287 |
1288 | configure_sizes()
1289 |
1290 |
1291 | def fix_status_bar_shading(mesg):
1292 | status_bar.show()
1293 |
1294 | status_bar.messageChanged.connect(fix_status_bar_shading)
1295 | status_bar.showMessage('Starting', 1)
1296 | status_bar.hide()
1297 | if errors:
1298 | status_bar.showMessage('. '.join(e.args[0] for e in errors), 10000)
1299 |
1300 | #ui_toplevel.right_layout = QtWidgets.QStackedLayout(main_widget.layout())
1301 | main_widget.left_layout.addWidget(map_widget)
1302 | main_widget.left_layout.addWidget(info_widget)
1303 | #main_widget.left_layout.addWidget(status_bar)
1304 | #main_widget.layout().addWidget(ui_toplevel.settings_widget, 2, 0)
1305 | main_widget.right_splitter.addWidget(text_edit_0)
1306 | main_widget.right_splitter.addWidget(ui_toplevel.settings_widget)
1307 | main_widget.right_layout.addWidget(status_bar)
1308 | #main_widget.layout().addWidget(text_edit_1, 1, 1)
1309 |
1310 | main_widget.resize(COLUMN_WIDTH * 4, 600)
1311 |
1312 | main_widget.finalize()
1313 |
1314 | ui_toplevel.main_widget = main_widget
1315 |
1316 | ui_toplevel.main_window = main_window
1317 | if node.code_element.name != 'Root':
1318 | ui_toplevel.main_window.setWindowTitle(node.code_element.name)
1319 | else:
1320 | ui_toplevel.main_window.setWindowTitle('Call Map')
1321 |
1322 | QtWidgets.qApp.setWindowIcon(QtGui.QIcon(
1323 | str(Path(__file__).parent.joinpath('icons/cruciform-by-andylee.png'))))
1324 |
1325 | original_window_flags = ui_toplevel.main_window.windowFlags()
1326 |
1327 | if show_gui:
1328 | MAXIMIZE_ON_START = True
1329 | # the following is a workaround to fix window size when maximizing
1330 | if MAXIMIZE_ON_START:
1331 | ui_toplevel.main_window.setWindowFlags(Qt.Qt.FramelessWindowHint)
1332 | ui_toplevel.main_window.setWindowState(Qt.Qt.WindowFullScreen)
1333 | ui_toplevel.main_window.setVisible(True)
1334 |
1335 | ui_toplevel.main_window.setWindowFlags(original_window_flags)
1336 | ui_toplevel.main_window.setWindowState(Qt.Qt.WindowMaximized)
1337 | ui_toplevel.main_window.setVisible(True)
1338 | else:
1339 | ui_toplevel.main_window.resize(1800, 1000)
1340 | ui_toplevel.main_window.show()
1341 |
1342 | map_widget.callLists[0].setFocus()
1343 |
1344 | def customFullScreen(self):
1345 | # experimental
1346 | self.setWindowFlags(Qt.Qt.FramelessWindowHint)
1347 | self.setWindowState(Qt.Qt.WindowFullScreen)
1348 | self.show()
1349 |
1350 | def cancelFullScreen(self):
1351 | self.setWindowFlags(original_window_flags)
1352 | self.show()
1353 |
1354 | ui_toplevel.main_window.customFullScreen = customFullScreen.__get__(ui_toplevel.main_window)
1355 | ui_toplevel.main_window.cancelFullScreen = cancelFullScreen.__get__(ui_toplevel.main_window)
1356 |
1357 | return ui_toplevel
1358 |
1359 |
1360 | def _resolve_robustly(str_paths: List[str]):
1361 | import os.path
1362 | paths = []
1363 | for ff in str_paths:
1364 | try:
1365 | pp = Path(ff).resolve()
1366 | except FileNotFoundError as err:
1367 | logger.error('Could not resolve {} from {}'.format(ff, os.path.realpath('.')))
1368 | else:
1369 | paths.append(pp)
1370 |
1371 | return paths
1372 |
1373 |
1374 | def main():
1375 | import argparse
1376 |
1377 | parser = argparse.ArgumentParser(
1378 | description='Create root node from filename contents')
1379 | parser.add_argument('-m', '--modules', metavar='M', type=str, nargs='+',
1380 | help='Modules (e.g. "os.path").',
1381 | default=[])
1382 | parser.add_argument('-f', '--files', metavar='F', type=str, nargs='+',
1383 | help='Script or module file names.',
1384 | default=[])
1385 | parser.add_argument('-p', '--add-to-sys-path', metavar='P', type=str,
1386 | nargs='+', help='''Directories to add to the analysis
1387 | Python module search path ("sys_path"), where modules
1388 | will be found during analysis. By default the Python
1389 | interpreter's `sys.path` is included. Note that the
1390 | module resolution order in Python is first match; earlier items in
1391 | `sys_path` have higher priority.''', default=[])
1392 | parser.add_argument('-d', '--project-directory', metavar='PROJ_DIR', action='store', type=Path,
1393 | default=None,
1394 | help=('''Where to store `call_map` bookmarks, modules, sys_path, etc.
1395 | If not set, these will not be saved.'''))
1396 | parser.add_argument('--no-interpreter-sys-path', action='store_true',
1397 | help='''Tells `call_map` not explicitly include the
1398 | `sys.path` from the interpreter in the Python module
1399 | search path. (Note that the analysis backend `jedi` as
1400 | of v0.10.0 will still fall back to the interpreter's
1401 | `sys.path` if it cannot resolve modules using the
1402 | `sys_path` that `call_map` passes to it.)
1403 | ''')
1404 | parser.add_argument('--ipython', action='store_true', help='''Enables
1405 | IPython integration. See
1406 | `dev_helper_tools/shell_tools.zsh` in the `call_map`
1407 | source tree.''')
1408 | parser.add_argument('-v', '--verbose', action='store_true', help='''Increase
1409 | logging verbosity.''')
1410 | parser.add_argument('--version', action='store_true', help='''Print version and exit.''')
1411 |
1412 | args = parser.parse_args()
1413 |
1414 | if args.version:
1415 | from . import version
1416 | print(version)
1417 | exit()
1418 |
1419 | if args.verbose:
1420 | logging.basicConfig(level=logging.INFO)
1421 | else:
1422 | logging.basicConfig(level=get_user_config()['LOG_LEVEL'])
1423 |
1424 | module_names = args.modules
1425 |
1426 | file_names = _resolve_robustly(args.files) # type: List[Path]
1427 |
1428 | additional_paths = _resolve_robustly(args.add_to_sys_path) # type: List[Path]
1429 |
1430 | user_scope_settings = UserScopeSettings(
1431 | module_names=module_names,
1432 | file_names=file_names,
1433 | include_runtime_sys_path=(not args.no_interpreter_sys_path),
1434 | add_to_sys_path=additional_paths)
1435 |
1436 | try:
1437 | ui_toplevel = make_app(user_scope_settings, project_directory=args.project_directory)
1438 | runtime_sys_modules[ui_toplevel.__name__] = ui_toplevel
1439 | enable_ipython_support = args.ipython
1440 |
1441 | if enable_ipython_support:
1442 | # Previously this code was to stop segfault on exit.
1443 | # It was tentatively removed around v0.2.0 due to improvements in ipython apparently obviating it.
1444 | #import atexit
1445 | #atexit.register(ui_toplevel.main_window.deleteLater)
1446 | pass
1447 | else:
1448 | exit(ui_toplevel.app.exec_())
1449 |
1450 | except (BadArgsError, ModuleResolutionError) as err:
1451 | if not args.ipython:
1452 | logger.error(err)
1453 | exit(1)
1454 | else:
1455 | raise
1456 |
--------------------------------------------------------------------------------
/call_map/icons/cruciform-by-andylee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/call_map/8f9b41260f654595e7520fade107d018ea011c0d/call_map/icons/cruciform-by-andylee.png
--------------------------------------------------------------------------------
/call_map/icons/statement-of-permission.txt:
--------------------------------------------------------------------------------
1 | In this document, "Call Map" refers to the program that was authored by Andrew
2 | J. Lee under employment of NCC Group PLC during the years 2016 and 2017, and
3 | bears the name "Call Map". The associated artifacts of "Call Map" are its
4 | source code, package specification code, user documentation, and developer
5 | documentation.
6 |
7 | Andrew J. Lee, henceforth referred to as THE LICENSOR, is the creator and
8 | copyright owner of the artwork contained in the file by the name of
9 | "cruciform-by-andylee.png" and having the SHA-256 value of
10 | 616283e2683bbb52e9ef18ed83d7844630c143a7006a99942cb81795db274d28, henceforth
11 | referred to as THE ARTWORK.
12 |
13 | THE LICENSOR grants nonexclusive permission to NCC Group PLC to display THE
14 | ARTWORK in association with the program "Call Map" and to include THE ARTWORK
15 | while distributing the program "Call Map" and the associated artifacts of the
16 | program "Call Map".
17 |
--------------------------------------------------------------------------------
/call_map/jedi_alt/JEDI_LICENSE.txt:
--------------------------------------------------------------------------------
1 | All contributions towards Jedi are MIT licensed.
2 |
3 | Some Python files have been taken from the standard library and are therefore
4 | PSF licensed. Modifications on these files are dual licensed (both MIT and
5 | PSF). These files are:
6 |
7 | - jedi/parser/pgen2
8 | - jedi/parser/tokenize.py
9 | - jedi/parser/token.py
10 | - test/test_parser/test_pgen2.py
11 |
12 | -------------------------------------------------------------------------------
13 | The MIT License (MIT)
14 |
15 | Copyright (c) <2013>
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 |
24 | The above copyright notice and this permission notice shall be included in
25 | all copies or substantial portions of the Software.
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
33 | THE SOFTWARE.
34 | -------------------------------------------------------------------------------
35 |
36 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
37 | --------------------------------------------
38 |
39 | 1. This LICENSE AGREEMENT is between the Python Software Foundation
40 | ("PSF"), and the Individual or Organization ("Licensee") accessing and
41 | otherwise using this software ("Python") in source or binary form and
42 | its associated documentation.
43 |
44 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby
45 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
46 | analyze, test, perform and/or display publicly, prepare derivative works,
47 | distribute, and otherwise use Python alone or in any derivative version,
48 | provided, however, that PSF's License Agreement and PSF's notice of copyright,
49 | i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
50 | 2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved"
51 | are retained in Python alone or in any derivative version prepared by Licensee.
52 |
53 | 3. In the event Licensee prepares a derivative work that is based on
54 | or incorporates Python or any part thereof, and wants to make
55 | the derivative work available to others as provided herein, then
56 | Licensee hereby agrees to include in any such work a brief summary of
57 | the changes made to Python.
58 |
59 | 4. PSF is making Python available to Licensee on an "AS IS"
60 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
61 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
62 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
63 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
64 | INFRINGE ANY THIRD PARTY RIGHTS.
65 |
66 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
67 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
68 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
69 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
70 |
71 | 6. This License Agreement will automatically terminate upon a material
72 | breach of its terms and conditions.
73 |
74 | 7. Nothing in this License Agreement shall be deemed to create any
75 | relationship of agency, partnership, or joint venture between PSF and
76 | Licensee. This License Agreement does not grant permission to use PSF
77 | trademarks or trade name in a trademark sense to endorse or promote
78 | products or services of Licensee, or any third party.
79 |
80 | 8. By copying, installing or otherwise using Python, Licensee
81 | agrees to be bound by the terms and conditions of this License
82 | Agreement.
83 |
--------------------------------------------------------------------------------
/call_map/jedi_alt/__init__.py:
--------------------------------------------------------------------------------
1 | from . import usages
2 |
--------------------------------------------------------------------------------
/call_map/jedi_alt/api_usages.py:
--------------------------------------------------------------------------------
1 | """
2 | Modified version of `jedi.api.usages` from `jedi` v0.10.0.
3 |
4 | - Removed :func:`resolve_potential_imports`.
5 | - Added `compare_contexts` and used it in `usages`. (This was merged upstream.)
6 | - Caught and logged errors in loop over usage items. This makes it so that if
7 | one item raises an error, the user can still see other usages. (This will not
8 | be merged upstream. Catching non-top-level errors is frowned upon in Jedi.)
9 |
10 | """
11 |
12 | from jedi.api import classes
13 | from jedi.parser import tree
14 | from jedi.evaluate import imports
15 | from jedi.evaluate.filters import TreeNameDefinition
16 | from jedi.evaluate.representation import ModuleContext
17 | import logging
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | def compare_contexts(c1, c2):
23 | return c1 == c2 or (c1[1] == c2[1] and c1[0].tree_node == c2[0].tree_node)
24 |
25 |
26 | def usages(evaluator, definition_names, mods):
27 | """
28 | :param definitions: list of Name
29 | """
30 | def resolve_names(definition_names):
31 | for name in definition_names:
32 | if name.api_type == 'module':
33 | found = False
34 | for context in name.infer():
35 | found = True
36 | yield context.name
37 | if not found:
38 | yield name
39 | else:
40 | yield name
41 |
42 | def compare_array(definition_names):
43 | """ `definitions` are being compared by module/start_pos, because
44 | sometimes the id's of the objects change (e.g. executions).
45 | """
46 | return [
47 | (name.get_root_context(), name.start_pos)
48 | for name in resolve_names(definition_names)
49 | ]
50 |
51 | search_name = list(definition_names)[0].string_name
52 | compare_definitions = compare_array(definition_names)
53 | mods = mods | set([d.get_root_context() for d in definition_names])
54 | definition_names = set(resolve_names(definition_names))
55 | for m in imports.get_modules_containing_name(evaluator, mods, search_name):
56 | if isinstance(m, ModuleContext):
57 | for name_node in m.tree_node.used_names.get(search_name, []):
58 | context = evaluator.create_context(m, name_node)
59 | try:
60 | result = evaluator.goto(context, name_node)
61 | except (NotImplementedError, RecursionError) as err:
62 | logger.error(err)
63 | continue
64 | if any(compare_contexts(c1, c2)
65 | for c1 in compare_array(result)
66 | for c2 in compare_definitions):
67 | name = TreeNameDefinition(context, name_node)
68 | definition_names.add(name)
69 | # Previous definitions might be imports, so include them
70 | # (because goto might return that import name).
71 | compare_definitions += compare_array([name])
72 | else:
73 | # compiled objects
74 | definition_names.add(m.name)
75 |
76 | return [classes.Definition(evaluator, n) for n in definition_names]
77 |
--------------------------------------------------------------------------------
/call_map/jedi_alt/monkey_patch_evaluate_compiled.py:
--------------------------------------------------------------------------------
1 | """
2 | Sometimes the unimplemented method `jedi.evaluate.CompiledObject.dict_values` is called.
3 | TODO: make tests and push upstream
4 | """
5 |
6 | import logging
7 | from jedi.evaluate import CompiledObject
8 |
9 | def do_monkey_patch():
10 | def _dict_values(self):
11 | logging.getLogger(__name__).warning(
12 | "Monkey patched function `jedi.evaluate.CompiledObject.dict_values` called.")
13 | return set([create(self.evaluator, val) for val in self.obj.values()])
14 |
15 | CompiledObject.dict_values = _dict_values
16 |
--------------------------------------------------------------------------------
/call_map/jedi_alt/stop_signal.py:
--------------------------------------------------------------------------------
1 | from jedi.evaluate.recursion import ExecutionRecursionDetector
2 |
3 | try:
4 | from queue import Queue
5 | except ImportError:
6 | # Python 2 shim
7 | from Queue import Queue
8 |
9 | stop_execution_signal_queue = Queue(maxsize=1)
10 | """
11 | Allows a controller thread to cause Jedi to abort execution.
12 | `ExecutionRecursionDetector.push_execution` will raise `StopExecutionException`
13 | if the `stop_execution_signal_queue` is not empty.
14 | """
15 |
16 | class StopExecutionException(Exception):
17 | """Raised when Jedi aborts execution"""
18 | pass
19 |
20 |
21 | def poll_and_handle_stop_execution_signal():
22 | if not stop_execution_signal_queue.empty():
23 | stop_execution_signal_queue.get()
24 | raise StopExecutionException('Received signal to stop execution.')
25 |
26 | def poll_and_handle_stop_execution_signal_at_start(function):
27 | def wrapper(obj, *args, **kwargs):
28 | poll_and_handle_stop_execution_signal()
29 | return function(obj, *args, **kwargs)
30 | return wrapper
31 |
32 |
33 | # Monkey patch jedi.
34 | # The pull request to push this functionality upstream was rejected.
35 | # See https://github.com/davidhalter/jedi/pull/898
36 | ExecutionRecursionDetector.push_execution = poll_and_handle_stop_execution_signal_at_start(ExecutionRecursionDetector.push_execution)
37 |
--------------------------------------------------------------------------------
/call_map/jedi_alt/usages.py:
--------------------------------------------------------------------------------
1 | from typing import List, Tuple
2 | import logging
3 |
4 | import jedi
5 | import jedi.api
6 |
7 | from jedi._compatibility import unicode
8 | from jedi.api import classes
9 |
10 | from jedi.evaluate import imports
11 | from jedi.evaluate.filters import TreeNameDefinition
12 | from jedi.evaluate.representation import ModuleContext
13 |
14 | from ..config import get_user_config
15 |
16 | if tuple(map(int, jedi.__version__.split('.'))) >= (0,10,1):
17 | from jedi.parser.python.tree import Import as tree_Import
18 | else:
19 | from jedi.parser.tree import Import as tree_Import
20 |
21 |
22 | PROFILING = get_user_config()['PROFILING']
23 |
24 |
25 | def usages_with_additional_modules(script: jedi.api.Script,
26 | additional_module_contexts: Tuple[ModuleContext] = ()):
27 | """
28 | Based on `jedi.api.Script.usages`, except `additional_modules` are also searched
29 |
30 | Forked from `jedi.api.Script.usages` on 2017-02-02.
31 |
32 | Return :class:`classes.Definition` objects, which contain all
33 | names that point to the definition of the name under the cursor. This
34 | is very useful for refactoring (renaming), or to show all usages of a
35 | variable.
36 |
37 | .. todo:: Implement additional_module_paths
38 |
39 | :rtype: list of :class:`classes.Definition`
40 | """
41 | from jedi import settings
42 | from jedi.api import usages
43 | from jedi.api import helpers
44 | from . import api_usages as alt_api_usages
45 |
46 | self = script
47 |
48 | temp, settings.dynamic_flow_information = \
49 | settings.dynamic_flow_information, False
50 | try:
51 | module_node = self._get_module_node()
52 | user_stmt = module_node.get_statement_for_position(self._pos)
53 | definition_names = self._goto()
54 |
55 | #assert not definition_names
56 | if not definition_names and isinstance(user_stmt, tree_Import):
57 | # For not defined imports (goto doesn't find something, we take
58 | # the name as a definition. This is enough, because every name
59 | # points to it.
60 | name = user_stmt.name_for_position(self._pos)
61 | if name is None:
62 | # Must be syntax
63 | return []
64 | definition_names = [TreeNameDefinition(self._get_module(), name)]
65 |
66 | if not definition_names:
67 | # Without a definition for a name we cannot find references.
68 | return []
69 |
70 | definition_names = usages.resolve_potential_imports(self._evaluator,
71 | definition_names)
72 |
73 | modules = set([d.get_root_context() for d in definition_names])
74 | modules.add(self._get_module())
75 | for additional_module_context in additional_module_contexts:
76 | modules.add(additional_module_context)
77 | definitions = alt_api_usages.usages(self._evaluator, definition_names, modules)
78 | finally:
79 | settings.dynamic_flow_information = temp
80 |
81 | return helpers.sorted_definitions(set(definitions))
82 |
83 |
84 | if PROFILING:
85 | try:
86 | from profilehooks import profile
87 | except ImportError:
88 | logging.getLogger(__name__).error('Failed to start with profiler; please install `profilehooks`.')
89 |
90 | usages_with_additional_modules = profile(usages_with_additional_modules, dirs=True, immediate=True)
91 |
--------------------------------------------------------------------------------
/call_map/jedi_ast_tools.py:
--------------------------------------------------------------------------------
1 | import jedi
2 | from typing import Tuple
3 |
4 |
5 | if tuple(map(int, jedi.__version__.split('.'))) >= (0,10,1):
6 | jedi_python_tree = jedi.parser.python.tree
7 | else:
8 | jedi_python_tree = jedi.parser.tree
9 | jedi.parser.tree.Node.get_first_leaf = jedi.parser.tree.Node.first_leaf
10 | jedi.parser.tree.Node.get_last_leaf = jedi.parser.tree.Node.last_leaf
11 |
12 |
13 | def leaves(node: jedi.parser.tree.Node):
14 | current_leaf = node.get_first_leaf()
15 | last_leaf = node.get_last_leaf()
16 |
17 | while True:
18 | yield current_leaf
19 |
20 | if current_leaf is last_leaf:
21 | break
22 | else:
23 | current_leaf = current_leaf.get_next_leaf()
24 |
25 |
26 | def walk_nodes(node: jedi.parser.tree.BaseNode):
27 | node = getattr(node, 'base', node)
28 |
29 | for elt in node.children:
30 | if isinstance(elt, jedi.parser.tree.BaseNode):
31 | yield elt
32 | yield from walk_nodes(elt)
33 |
34 |
35 | def walk_nodes_while_staying_in_scope(node: jedi.parser.tree.BaseNode):
36 | node = getattr(node, 'base', node)
37 |
38 |
39 | for elt in node.children:
40 | if isinstance(elt, jedi.parser.tree.BaseNode):
41 | yield elt
42 | if not isinstance(elt, jedi_python_tree.ClassOrFunc):
43 | yield from walk_nodes_while_staying_in_scope(elt)
44 |
45 |
46 | def is_call_trailer(node: jedi.parser.tree.Node):
47 | """Whether the node is a trailer bounded by parens"""
48 |
49 | if node.type == 'trailer':
50 | fl = node.get_first_leaf()
51 | ll = node.get_last_leaf()
52 |
53 | return (type(fl) is jedi_python_tree.Operator
54 | and fl.value == '('
55 | and type(ll) is jedi_python_tree.Operator
56 | and ll.value == ')')
57 |
58 | else:
59 | return False
60 |
61 |
62 | def _maybe_getattr_chain(obj, *attrs):
63 | for attr in attrs:
64 | obj = getattr(obj, attr, obj)
65 |
66 | return obj
67 |
68 |
69 | def decorator_name(node: jedi.parser.tree.BaseNode):
70 | child_1 = node.children[1]
71 | if isinstance(child_1, jedi_python_tree.Name):
72 | return child_1
73 | else:
74 | name = child_1.children[-1]
75 | assert isinstance(name, jedi_python_tree.Name)
76 | return name
77 |
78 |
79 | def vec_add(pos0: Tuple[int, int], pos1: Tuple[int, int]):
80 | return (pos0[0] + pos1[0], pos0[1] + pos1[1])
81 |
82 |
83 | def get_called_functions(node: jedi.parser.tree.BaseNode):
84 | """Yield AST leaves that represent called functions."""
85 |
86 | _abort = (type(_maybe_getattr_chain(node, 'base', 'var'))
87 | is jedi.evaluate.compiled.CompiledObject)
88 |
89 | if not _abort:
90 | for child in walk_nodes_while_staying_in_scope(node):
91 | if isinstance(child, jedi_python_tree.ClassOrFunc):
92 | name = child.name
93 |
94 | if isinstance(child, jedi_python_tree.Lambda):
95 | loc = (vec_add((-0,0), child.start_pos),
96 | vec_add((-0,0), child.end_pos))
97 | else:
98 | loc = ((child.start_pos[0], name.start_pos[1]), name.end_pos)
99 |
100 | yield ('definition', name, child) + loc
101 | elif is_call_trailer(child):
102 | name = child.get_previous_leaf()
103 | yield 'child', name, child, name.start_pos, name.end_pos
104 | elif isinstance(child, jedi_python_tree.Decorator):
105 | name = decorator_name(child)
106 | yield 'child', name, child, name.start_pos, name.end_pos
107 |
108 |
109 | def parent_scope_of_usage(usage: jedi.api.classes.Definition) -> jedi.api.classes.Definition:
110 | # Like Definition.parent() but skips nameless parents.
111 | # Test case: jeid.evaluate.docstrings._evaluate_for_statement_string.
112 | try:
113 | return usage.parent()
114 | except AttributeError:
115 | _name = usage._name
116 | while not hasattr(_name, 'name'):
117 | _name = _name.get_parent_scope()
118 | return jedi.api.classes.Definition(usage._evaluator, usage._evaluator.wrap(_name).name)
119 |
120 |
121 | def _convert_FunctionExecutionContext_to_FunctionContext(_evaluator, context):
122 | '''Helper copied from `jedi.api.classes.Definition.parent` for `parent_definition`'''
123 |
124 | from jedi.evaluate import representation as er
125 |
126 | if isinstance(context, er.FunctionExecutionContext):
127 | # TODO the function context should be a part of the function
128 | # execution context.
129 | context = er.FunctionContext(
130 | _evaluator, context.parent_context, context.tree_node)
131 |
132 | return context
133 |
134 |
135 | def parent_definition(definition: jedi.api.classes.Definition) -> jedi.api.classes.Definition:
136 | '''Robust version of `jedi.api.classes.Denition.parent`'''
137 | context = _convert_FunctionExecutionContext_to_FunctionContext(definition._evaluator,
138 | definition._name.parent_context)
139 | if context is None:
140 | return definition
141 |
142 | while not hasattr(context, 'name'):
143 | new_context = _convert_FunctionExecutionContext_to_FunctionContext(definition._evaluator,
144 | context.parent_context)
145 | if context is not None:
146 | context = new_context
147 | else:
148 | break
149 |
150 | return jedi.api.classes.Definition(definition._evaluator, context.name)
151 |
--------------------------------------------------------------------------------
/call_map/jedi_dump.py:
--------------------------------------------------------------------------------
1 | """
2 | *Warning* this module uses jedi internals and is highly dependent on the jedi
3 | version. Please refer to jedi-requirements.txt in the toplevel of the repo for
4 | the version this commit is compatible with.
5 |
6 | """
7 |
8 | import logging
9 | import pprint, textwrap
10 | from sys import path as actual_sys_path
11 |
12 | from typing import List, Dict, Tuple, Callable, Any, Optional
13 | from pathlib import Path
14 |
15 | from .core import CodeElement, Node, OrganizerNode, UserScopeSettings, ScopeSettings
16 | from . import config
17 | from . import jedi_alt
18 |
19 | import toolz as tz
20 |
21 | import jedi
22 | from .jedi_ast_tools import get_called_functions, parent_definition
23 | from .jedi_alt.stop_signal import stop_execution_signal_queue, StopExecutionException
24 |
25 | logger = logging.getLogger(__name__)
26 |
27 |
28 | def catch_errors(thunk: Callable, default: Any, post_message: str):
29 | try:
30 | return thunk()
31 | except StopExecutionException as exc:
32 | logger.info('{}; {}'.format(exc, post_message))
33 | return default
34 | except Exception as exc:
35 | logger.error('{}; {}'.format(exc, post_message), exc_info=config.get_user_config()['EXC_INFO'])
36 | #import traceback
37 | #logger.error(traceback.format_exc())
38 | return default
39 |
40 |
41 | def filter_nodes(nodes):
42 | for node in nodes:
43 | try:
44 | skip = (node.code_element.name in
45 | config.py_ignore.get(node.code_element.module, ()))
46 | except AttributeError:
47 | skip = False
48 |
49 | if skip:
50 | continue
51 | else:
52 | yield node
53 |
54 |
55 | def _cleanup_signal_queue():
56 | '''Cleans signal queue in case reference resolution finished successfully
57 |
58 | Call after finishing reference resolution searches. This cleans up any
59 | leftover signals, in case a "stop" signal was issued, but the resolution
60 | search actually completed successfully.
61 |
62 | '''
63 |
64 | while not stop_execution_signal_queue.empty():
65 | stop_execution_signal_queue.get()
66 |
67 |
68 | class UsageFilter(jedi.evaluate.filters.ParserTreeFilter):
69 | def _is_name_reachable(self, name):
70 | #print(name, name.parent.type)
71 | parent = name.parent
72 | if parent.type in ('decorator', 'atom_expr', 'trailer'):
73 | return True
74 |
75 | if not name.is_definition():
76 | return False
77 |
78 | if parent.type == 'trailer':
79 | return True
80 | base_node = parent if parent.type in ('classdef', 'funcdef') else name
81 | return base_node.get_parent_scope() == self._parser_scope
82 |
83 |
84 | class JediCodeElementNode(Node):
85 | usage_resolution_modules = frozenset() # where to search for usages
86 | sys_path = []
87 |
88 | def __init__(self, code_element: CodeElement, definition: jedi.api.classes.Definition):
89 | """The parents call the node, children are called by the node.
90 |
91 | If not callable, the Node has no children
92 | """
93 | self.code_element = code_element
94 | self.definition = definition
95 |
96 | @property
97 | def module_context(self):
98 | if self.definition:
99 | return self.definition._name.get_root_context()
100 | else:
101 | return None
102 |
103 | def __repr__(self):
104 | return '<{}({}, call_pos={})>'.format(self.__class__.__name__, self.code_element.name, self.code_element.call_pos)
105 |
106 | @property
107 | def parents(self):
108 | # Note: the last created Script object appears to bork the older ones. Must keep making new Script objects!
109 |
110 | # Note: jedi appears to already do enough caching. It does not significantly
111 | # improve performance to cache the parents.
112 |
113 | #acceptable_name_types = (jedi.parser.tree.Name,
114 | # jedi.evaluate.representation.InstanceElement)
115 |
116 | if self.definition and self.definition.module_path:
117 | script = jedi.api.Script(source_path=self.definition.module_path,
118 | sys_path=self.definition._evaluator.sys_path,
119 | line=self.definition.line,
120 | column=self.definition.column)
121 |
122 | usages = catch_errors(tz.partial(jedi_alt.usages.usages_with_additional_modules,
123 | script,
124 | self.usage_resolution_modules),
125 | [],
126 | 'while finding usages of {}'.format(self.code_element.name))
127 |
128 | elif self.code_element.call_pos[0]:
129 | call_pos_script = jedi.api.Script(source_path=self.code_element.call_pos[0],
130 | sys_path=self.definition._evaluator.sys_path if self.definition else self.sys_path,
131 | line=self.code_element.call_pos[1][0],
132 | column=self.code_element.call_pos[1][1])
133 |
134 | usages = catch_errors(tz.partial(jedi_alt.usages.usages_with_additional_modules,
135 | call_pos_script,
136 | self.usage_resolution_modules),
137 | [],
138 | 'while finding usages of {}'.format(self.code_element.name))
139 |
140 | elif self.definition:
141 | script = create_import_script(self.definition._evaluator.sys_path if self.definition else self.sys_path,
142 | self.code_element.name)
143 |
144 | usages = [
145 | usage for usage in
146 | catch_errors(tz.partial(jedi_alt.usages.usages_with_additional_modules,
147 | script,
148 | self.usage_resolution_modules),
149 | [],
150 | 'while finding usages of {}'.format(self.code_element.name))
151 | if usage.module_name]
152 |
153 | else:
154 | return ()
155 |
156 | _unfiltered_parents = []
157 | positions = set()
158 |
159 | for usage in usages:
160 | tree_name = usage._name.tree_name
161 | if tree_name:
162 | position = (usage.module_path, tree_name.start_pos, tree_name.end_pos)
163 | else:
164 | position = (None, (None, None), (None, None))
165 |
166 | if position not in positions or position == (None, (None, None), (None, None)):
167 | _usage_parent = parent_definition(usage)
168 |
169 | if _usage_parent.module_path:
170 | JediCodeElementNode.usage_resolution_modules |= frozenset((_usage_parent._name.get_root_context(),))
171 |
172 | usage_node = JediCodeElementNode.from_definition(
173 | 'parent', position, _usage_parent)
174 |
175 | # check if this usage is actually the definition of the
176 | # current node, and is therefore already covered by the
177 | # "- [sig]" node.
178 | if (usage_node.code_element.call_pos[0] == self.code_element.path
179 | and usage_node.code_element.call_pos[1] == self.code_element.start_pos
180 | and usage_node.code_element.type == 'module'):
181 |
182 | logger.info('Usages: Skipped definition of {} at {}:{}.'
183 | .format(self.code_element.name,
184 | usage_node.code_element.name,
185 | usage_node.code_element.call_pos[1][0]))
186 | continue
187 | else:
188 | _unfiltered_parents.append(usage_node)
189 | positions.add(position)
190 |
191 | _cleanup_signal_queue()
192 |
193 | return _unfiltered_parents
194 |
195 |
196 | @property
197 | def children(self):
198 | if self.definition:
199 |
200 | ## If self is a package, yield submodules/subpackages
201 | if self.code_element.type == 'module' and self.code_element.path:
202 | _pp = Path(self.code_element.path)
203 |
204 | sys_path = self.definition._evaluator.sys_path
205 |
206 | is_pkg = (self.code_element.name != _pp.stem
207 | and self.code_element.name == _pp.parent.stem
208 | and _pp.stem == '__init__')
209 |
210 | if is_pkg:
211 |
212 | submodules = [path_to_module_name(sys_path, str(_m)) for _m in _pp.parent.glob('*.py')
213 | if _m != _pp]
214 |
215 | subdirs = [_dd for _dd in _pp.parent.iterdir()
216 | if _dd.is_dir() and _dd.name != '__pycache__']
217 |
218 | subpkgs = [_dd for _dd in subdirs if _dd.joinpath('__init__.py').exists()]
219 |
220 | if len(subpkgs) < len(subdirs):
221 | logger.warning('Package `{}` contains subdirectories that are not packages:\n{}'.format(
222 | self.code_element.name,
223 | textwrap.indent(
224 | pprint.pformat([str(_dd.stem) for _dd in set(subdirs) - set(subpkgs)]),
225 | ' ' * 4)))
226 |
227 | names = submodules + [path_to_module_name(sys_path, str(_dd)) for _dd in subpkgs]
228 |
229 | for _name in names:
230 | _node, _err = get_module_node(sys_path, _name)
231 | try:
232 | yield _node
233 | except KeyError:
234 | continue
235 |
236 | if config.get_user_config()['EXPERIMENTAL_MODE']:
237 | yield from experimental_definitions_of_called_objects(self.definition)
238 | else:
239 | tree_name = self.definition._name.tree_name
240 | if not tree_name:
241 | for inferred in self.definition._name.infer():
242 | tree_name = inferred.tree_node
243 | if tree_name:
244 | break
245 |
246 | if tree_name:
247 | tree_definition = tree_name.get_definition()
248 |
249 | path = self.definition.module_path
250 | _unfiltered = definitions_of_called_objects(self.definition._evaluator, tree_definition, path)
251 |
252 | yield from filter_nodes(_unfiltered)
253 |
254 | _cleanup_signal_queue()
255 |
256 | @staticmethod
257 | def cancel_search():
258 | if stop_execution_signal_queue.empty():
259 | stop_execution_signal_queue.put(1)
260 |
261 | @classmethod
262 | def from_definition(cls, role, call_pos, definition):
263 | #if not isinstance(definition._name, jedi.parser.tree.Name):
264 | # name = definition._name.name
265 | #else:
266 | # name = definition._name
267 |
268 | start_pos = (definition.line, definition.column)
269 | if definition._name.tree_name:
270 | end_pos = definition._name.tree_name.end_pos or (None, None)
271 | else:
272 | end_pos = (None, None)
273 |
274 | code_element = CodeElement(
275 | name=definition.name,
276 | type=definition.type,
277 | module=definition.module_name,
278 | role=role,
279 | path=definition.module_path,
280 | call_pos=call_pos,
281 | start_pos=start_pos,
282 | end_pos=end_pos,
283 | )
284 |
285 | return cls(code_element, definition)
286 |
287 | def with_new_role(self, role):
288 | if role == 'signature':
289 | new_call_pos = (self.code_element.path, self.code_element.start_pos, self.code_element.end_pos)
290 | new_code_element = self.code_element._replace(role=role, call_pos=new_call_pos)
291 | return __class__(new_code_element, self.definition)
292 | else:
293 | raise NotImplementedError
294 |
295 |
296 | def definitions_of_called_objects(evaluator: jedi.evaluate.Evaluator,
297 | definition: jedi.parser.tree.BaseNode,
298 | path: str):
299 |
300 | for role, fn, ast_node, call_start_pos, call_end_pos in get_called_functions(definition):
301 | #try:
302 | # defs = list(jedi.api.helpers.evaluate_goto_definition(evaluator, fn))
303 | #except AttributeError:
304 | # defs = list()
305 |
306 | code = ast_node.get_root_node().get_code()
307 | script = jedi.api.Script(source=code, source_path=path,
308 | sys_path=evaluator.sys_path,
309 | line=call_start_pos[0],
310 | column=call_start_pos[1])
311 |
312 | defs = (catch_errors(script.goto_definitions, [], 'while finding definitions of {}'.format(fn))
313 | or catch_errors(script.goto_assignments, [], 'while finding assignments of {}'.format(fn)))
314 |
315 | found = set()
316 |
317 | call_pos = (path, call_start_pos, call_end_pos)
318 |
319 | for ii, def_ in enumerate(defs):
320 | #def_ = defs[-1] # should be jedi.evaluate.representation.Function
321 |
322 | assert isinstance(def_, jedi.api.classes.Definition)
323 |
324 | module = def_._module
325 | called_fn_def = def_
326 | base_name = def_.name
327 | try:
328 | start_pos, end_pos = (def_._name.tree_name.start_pos,
329 | def_._name.tree_name.end_pos)
330 | except AttributeError:
331 | start_pos, end_pos = ((None, None), (None, None))
332 |
333 | defined_at = (def_.module_path, start_pos, end_pos)
334 |
335 | if (base_name, defined_at) in found:
336 | continue
337 | else:
338 | found.add((base_name, defined_at))
339 |
340 | if ii > 0:
341 | name = base_name + ' ({})'.format(ii + 1)
342 | else:
343 | name = base_name
344 |
345 | code_element = CodeElement(
346 | name=name,
347 | type=called_fn_def.type,
348 | module=module.name.string_name,
349 | role = role,
350 | path = defined_at[0],
351 | call_pos = call_pos,
352 | start_pos=defined_at[1],
353 | end_pos=defined_at[2],
354 | )
355 |
356 | yield JediCodeElementNode(code_element, called_fn_def)
357 |
358 | if len(defs) == 0:
359 | logging.getLogger(__name__).debug(
360 | ' Cannot get def for {}'.format(fn))
361 |
362 | name = fn.value if fn.value not in (']', ')') else '[unknown]'
363 |
364 | if role == 'definition':
365 | start_pos = ast_node.start_pos
366 | end_pos = ast_node.end_pos
367 | else:
368 | start_pos = fn.start_pos
369 | end_pos = fn.end_pos
370 |
371 | code_element = CodeElement(
372 | name=name,
373 | type='[Unknown]',
374 | module='[Unknown]',
375 | role = role,
376 | path = None,
377 | call_pos = call_pos,
378 | start_pos=(None,None),
379 | end_pos=(None,None),
380 | )
381 |
382 | jn = JediCodeElementNode(code_element, None)
383 |
384 | yield jn
385 |
386 |
387 | def experimental_definitions_of_called_objects(definition: jedi.api.classes.Definition):
388 | _unfiltered_nodes = []
389 |
390 | for context in definition._name.infer():
391 | _filter = UsageFilter(definition._evaluator, context, node_context=None,
392 | until_position=None, origin_scope=None)
393 | values = jedi.api.classes._sort_names_by_start_pos(_filter.values())
394 |
395 | for tree_name_definition in values:
396 | name = jedi.api.classes.Definition(definition._evaluator, tree_name_definition)
397 | call_pos = (name.module_path, tree_name_definition.start_pos, tree_name_definition.tree_name.end_pos)
398 |
399 | assignments = name.goto_assignments()
400 |
401 | if assignments:
402 | assignment = assignments[0]
403 | else:
404 | assignment = name
405 |
406 | role = {'atom_expr': 'child',
407 | 'trailer': 'child',
408 | 'funcdef': 'definition',
409 | 'classdef': 'definition',
410 | }.get(tree_name_definition.tree_name.parent.type, 'definition')
411 |
412 | code_element = CodeElement(
413 | name=name.name,
414 | type=name.type,
415 | module=assignment.module_name,
416 | role = role,
417 | path = assignment.module_path,
418 | call_pos = call_pos,
419 | start_pos=name._name.start_pos,
420 | end_pos=name._name.tree_name.end_pos,
421 | )
422 |
423 | node = JediCodeElementNode(code_element, name)
424 | _unfiltered_nodes.append(node)
425 |
426 | yield from filter_nodes(_unfiltered_nodes)
427 |
428 |
429 | def _is_submodule(path: Path, parent: Path):
430 | for _parent in path.parents:
431 | if _parent == parent:
432 | return True
433 | elif _parent.joinpath('__init__.py').exists():
434 | continue
435 | else:
436 | return False
437 | else:
438 | return False
439 |
440 | def path_to_module_name(sys_path, path: str):
441 | path = Path(path).resolve()
442 | for _sp in reversed(sys_path):
443 | try:
444 | sp = Path(_sp).resolve()
445 | except FileNotFoundError:
446 | continue
447 |
448 | if _is_submodule(path, sp):
449 | module_name = ('.'.join(path.relative_to(sp).parts))
450 | if module_name.endswith('.py'):
451 | module_name = module_name[:-3]
452 | return module_name
453 | else:
454 | return None
455 |
456 |
457 | def create_import_script(effective_sys_path: List[Path], module_name: str) -> jedi.api.Script:
458 | import_script_text = 'import {}'.format(module_name)
459 | import_script = jedi.api.Script(source=import_script_text, sys_path=list(map(str, effective_sys_path)),
460 | line=1, column=len(import_script_text)-1) # TODO: double check column
461 | return import_script
462 |
463 |
464 | def get_module_node(effective_sys_path: List[Path], module_name: str) -> Tuple[Optional[Node], Optional[Exception]]:
465 | from .errors import ModuleResolutionError
466 |
467 | import_script = create_import_script(effective_sys_path, module_name)
468 | definitions = import_script.goto_definitions()
469 |
470 | if definitions:
471 | mod = tz.first(definitions)
472 |
473 | if tuple(map(int, jedi.__version__.split('.'))) >= (0,10,1):
474 | # duck punch to avoid mod._name.api_type error, which uses parent_context.
475 | mod._name.parent_context = mod._name.get_root_context()
476 |
477 | if mod.module_path:
478 | JediCodeElementNode.usage_resolution_modules |= frozenset((mod._name.get_root_context(),))
479 |
480 | node = JediCodeElementNode.from_definition(
481 | role='definition',
482 | call_pos=(mod.module_path, (1,0), (None,None)),
483 | definition=mod)
484 |
485 | err = None
486 | else:
487 | node = None
488 | err = ModuleResolutionError(
489 | 'Could not resolve module {} (did you mean to use "-f"?)'.format(module_name))
490 |
491 | return node, err
492 |
493 |
494 | def dump_module_nodes(effective_sys_path: List[Path], module_names: List[str]) \
495 | -> Tuple[Dict[str, Node], Dict[str, Node]]:
496 |
497 | _nodes = {}
498 | failures = {}
499 |
500 | for _name in module_names:
501 | node, err = get_module_node(effective_sys_path, _name)
502 |
503 | if node:
504 | _nodes[_name] = node
505 | else:
506 | failures[_name] = err
507 |
508 | return _nodes, failures
509 |
510 |
511 | def remove_dupes(ll: list):
512 | seen = set()
513 | out = []
514 |
515 | for elt in ll:
516 | if elt not in seen:
517 | seen.add(elt)
518 | out.append(elt)
519 | else:
520 | continue
521 |
522 | return out
523 |
524 |
525 | def make_scope_settings(is_new_project: bool,
526 | saved_scope_settings: ScopeSettings,
527 | user_scope_settings: UserScopeSettings) -> ScopeSettings:
528 |
529 | additional_sys_path = list(map(str, user_scope_settings.add_to_sys_path)) # type: List[str]
530 |
531 | if is_new_project:
532 | effective_sys_path = saved_scope_settings.effective_sys_path + additional_sys_path
533 | elif user_scope_settings.include_runtime_sys_path:
534 | effective_sys_path = actual_sys_path + additional_sys_path
535 | else:
536 | effective_sys_path = additional_sys_path
537 |
538 | if '' in effective_sys_path:
539 | del effective_sys_path[effective_sys_path.index('')]
540 |
541 | module_names = remove_dupes(saved_scope_settings.module_names + user_scope_settings.module_names)
542 |
543 | scripts = saved_scope_settings.scripts.copy() # type: List[Path]
544 |
545 | for path in user_scope_settings.file_names:
546 | module_name = path_to_module_name(effective_sys_path, path)
547 | if module_name is not None:
548 | module_names.append(module_name)
549 | elif path.is_file():
550 | scripts.append(path)
551 | else:
552 | logger.warning('Could not resolve module for {}'.format(path))
553 |
554 | return ScopeSettings(module_names=module_names,
555 | scripts=scripts,
556 | effective_sys_path=list(map(Path, effective_sys_path)))
557 |
558 |
559 | def dump_script_nodes(effective_sys_path: List[Path], scripts: List[Path]) -> Dict[Path, Node]:
560 | from .errors import ScriptResolutionError
561 |
562 | _sys_path_str = list(map(str, effective_sys_path)) # type: List[str]
563 |
564 | failures = {}
565 | _nodes = {} # type: Dict[Path, Node]
566 | for path in scripts:
567 | fname = str(path)
568 | try:
569 | script = jedi.api.Script(path=fname, sys_path=_sys_path_str)
570 | except FileNotFoundError:
571 | failures[path] = ScriptResolutionError('Cannot resolve path to script "{}"'.format(fname))
572 | continue
573 | except UnicodeDecodeError:
574 | failures[path] = ScriptResolutionError('Cannot decode script "{}"'.format(fname))
575 | continue
576 |
577 | module = script._get_module()
578 |
579 | children = list(
580 | filter_nodes(
581 | definitions_of_called_objects(script._evaluator, module.tree_node, path=fname)))
582 |
583 | node = OrganizerNode(fname, [], children)
584 |
585 | node.code_element = CodeElement(
586 | name=module.name.string_name,
587 | type='script',
588 | module=module.name.string_name,
589 | role='definition',
590 | path=str(path.resolve()),
591 | call_pos=(fname, (None,None), (None,None)),
592 | start_pos=(1,0),
593 | end_pos=(None, None),
594 | )
595 |
596 | node.module_context = module
597 |
598 | _nodes[path] = node
599 |
600 | return _nodes, failures
601 |
--------------------------------------------------------------------------------
/call_map/package_info.json:
--------------------------------------------------------------------------------
1 | {"version": "1.0.2"}
2 |
--------------------------------------------------------------------------------
/call_map/project_settings_module.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import json
3 | import logging
4 | from typing import List, GenericMeta, Any, Tuple, Optional, Dict, Iterable
5 | import toolz as tz
6 |
7 | from .core import UserScopeSettings, ScopeSettings, CodeElement, CallPosType, Node
8 | from . import serialize
9 | from .custom_typing import CheckableOptional, CheckableDict, CheckableTuple, CheckableList, matches_spec
10 |
11 |
12 | project_settings = 'project_settings'
13 | sys_path = 'sys_path'
14 | bookmarks = 'bookmarks'
15 |
16 | # `files` can only changed manually
17 | # `modules` and `scripts` can be affected by `files` but does affect `files`
18 | files = 'files'
19 | modules = 'modules'
20 | scripts = 'scripts'
21 |
22 | categories = [project_settings, sys_path, bookmarks, modules, files, scripts]
23 |
24 | logger = logging.getLogger(__name__)
25 |
26 | category_type = {
27 | project_settings: CheckableDict({'project_directory': CheckableOptional(Path)}),
28 | modules: CheckableList(str),
29 | files: CheckableList(Path),
30 | sys_path: CheckableList(Path),
31 | bookmarks: CheckableList(CheckableList(CodeElement)),
32 | scripts: CheckableList(Path),
33 | }
34 |
35 |
36 | def diff(type_spec, oo_1, oo_2):
37 | if isinstance(type_spec, CheckableList):
38 | return ([elt_1 for elt_1 in oo_1 if elt_1 not in oo_2], [elt_2 for elt_2 in oo_2 if elt_2 not in oo_1])
39 | elif isinstance(type_spec, CheckableTuple):
40 | return [(elt_1, elt_2) for elt_1, elt_2 in zip(oo_1, oo_2) if elt_1 != elt_2]
41 | elif isinstance(type_spec, CheckableDict):
42 | return {key: (oo_1.get(key), oo_2.get(key)) for key in set(oo_1.keys()) + set(oo_2.keys())}
43 |
44 |
45 | class Project:
46 | def __init__(self, project_directory: Optional[Path]):
47 |
48 | self.project_files = {}
49 |
50 | self.settings = {category: _type.new_empty_instance() for category, _type in category_type.items()}
51 | #self.settings[project_settings] = {}
52 |
53 | self.project_directory_is_set_up = False # only matters for nontrivial project_directory
54 | self.project_directory = project_directory
55 |
56 | # each Node has attributes `code_element` and `definition`.
57 | self.failures = {}
58 | self.script_nodes = {} # type: Dict[Path, Node]; maps script path to node
59 | self.module_nodes = {} # type: Dict[str, Node]; maps module name to node
60 |
61 | @property
62 | def project_directory(self) -> Optional[Path]:
63 | try:
64 | return self.settings[project_settings]['project_directory']
65 | except KeyError:
66 | return None
67 |
68 | @project_directory.setter
69 | def project_directory(self, new_project_directory: Optional[Path]):
70 |
71 | if new_project_directory != self.project_directory:
72 | self.project_directory_is_set_up = False
73 | self.settings[project_settings]['project_directory'] = new_project_directory
74 |
75 | if new_project_directory:
76 | self.project_files.update({rest_name: new_project_directory.joinpath(rest_name + '.json')
77 | for rest_name in categories})
78 |
79 | def load_scope_settings(self, scope_settings: ScopeSettings):
80 | self.settings[modules] = scope_settings.module_names
81 | self.settings[scripts] = scope_settings.scripts
82 | self.settings[sys_path] = scope_settings.effective_sys_path
83 |
84 | @property
85 | def scope_settings(self):
86 | return ScopeSettings(module_names=self.settings[modules],
87 | scripts=self.settings[scripts],
88 | effective_sys_path=self.settings[sys_path])
89 |
90 | def update_scope_settings(self, scope_settings: ScopeSettings):
91 | self.update_settings({modules: scope_settings.module_names,
92 | scripts: scope_settings.scripts,
93 | sys_path: scope_settings.effective_sys_path})
94 |
95 | def encode(self, category: str, for_persistence: bool):
96 | '''Encode settings in JSON serializable format
97 |
98 | :param str category: category of settings to encode.
99 | :param bool for_persistence: whether to filter out settings that don't need to persist
100 |
101 | '''
102 | if for_persistence and category == project_settings:
103 | _settings = {k:v for k,v in self.settings[category].items()
104 | if k != 'project_directory'}
105 | else:
106 | _settings = self.settings[category]
107 |
108 | _type = category_type[category]
109 |
110 | return serialize.encode(_type, _settings)
111 |
112 | def update_persistent_storage(self):
113 | '''Write out to persistent storage
114 |
115 | Raises `FileNotFoundError` if the project directory cannot be created.
116 |
117 | '''
118 | if self.project_directory:
119 | if not self.project_directory_is_set_up:
120 | self._setup_project_directory()
121 | self.project_directory_is_set_up = True
122 |
123 | self._write_to_project_directory()
124 |
125 | def _setup_project_directory(self):
126 | # only used in update_persistent_storage
127 | try:
128 | self.project_directory.mkdir(mode=0o744, exist_ok=True)
129 | except FileNotFoundError:
130 | raise FileNotFoundError('Could not create project directory: {}'.format(self.project_directory))
131 |
132 | backup_directory = self.project_directory.joinpath('backup')
133 | try:
134 | backup_directory.mkdir(mode=0o744, exist_ok=True)
135 | except FileNotFoundError:
136 | raise FileNotFoundError('Could not create project backup directory: {}'.format(backup_directory))
137 |
138 | for ff in self.project_files.values():
139 | ff.touch(mode=0o644)
140 |
141 | def _write_to_project_directory(self):
142 | # only used in update_persistent_storage
143 | for key, project_file in self.project_files.items():
144 | if project_file.exists():
145 | backup = self.project_directory.joinpath('backup').joinpath(project_file.name)
146 | project_file.rename(backup)
147 |
148 | project_file = self.project_files[key]
149 | text = json.dumps(self.encode(key, for_persistence=True), indent=True, sort_keys=True)
150 | project_file.write_text(text + '\n')
151 |
152 | def load_from_persistent_storage(self):
153 | # precedence: files > (modules, scripts)
154 |
155 | # Note that `files` will only be changed manually
156 | # It is not updated by changing `modules` or `scripts`.
157 | # However, `files` will affect `modules` and `scripts`.
158 |
159 | decoded = {}
160 | if self.project_directory and self.project_directory.exists():
161 | for key, project_file in self.project_files.items():
162 | try:
163 | decoded[key] = serialize.decode(category_type[key], json.loads(project_file.read_text()))
164 | except json.JSONDecodeError as err:
165 | logger.error(err)
166 | except FileNotFoundError as err:
167 | #logger.error(err)
168 | pass
169 |
170 | return decoded
171 |
172 | def update_settings(self, new_settings: Dict[str, Any]):
173 | try:
174 | new_project_directory = new_settings[project_settings]['project_directory']
175 | except KeyError:
176 | pass
177 | else:
178 | self.project_directory = new_project_directory
179 |
180 | for category, value in new_settings.items():
181 | type_spec = category_type[category]
182 | settings = self.settings[category]
183 |
184 | if isinstance(type_spec, CheckableDict):
185 | settings.update(value)
186 | elif isinstance(type_spec, CheckableList):
187 | for elt in value:
188 | if elt not in settings:
189 | settings.append(elt)
190 | else:
191 | raise TypeError('Invalid category type {}'.format(type_spec))
192 |
193 |
194 | def set_settings(self, category: str, decoded: Any) -> Tuple[bool, Iterable[Node], Iterable[Node]]:
195 | from . import jedi_dump
196 |
197 | type_spec = category_type[category]
198 |
199 | if not matches_spec(decoded, type_spec):
200 | raise TypeError('Settings for {} should have type `{}`'.format(category, type_spec))
201 |
202 | if category == project_settings:
203 | self.project_directory = decoded['project_directory']
204 | return (True, [], [])
205 |
206 | old = self.settings[category]
207 |
208 | if isinstance(type_spec, CheckableList):
209 | to_delete = [elt for elt in old if elt not in decoded] # type: List[str]
210 | to_add = [elt for elt in decoded if elt not in old] # type: List[str]
211 |
212 | if category == modules:
213 | stale = []
214 | additional, failures = jedi_dump.dump_module_nodes(self.settings[sys_path], to_add)
215 |
216 | # Execute destructive operations now that we are done with
217 | # failure-prone operations
218 | self.settings[category] = decoded
219 |
220 | for name in to_delete:
221 | stale_node = self.module_nodes['python'].pop(name)
222 | stale.append(stale_node)
223 |
224 | self.module_nodes['python'].update(additional)
225 | self.failures['python'][category].update(failures)
226 |
227 | self.update_usage_search_locations('python')
228 |
229 | return (stale or additional, stale, additional.values())
230 |
231 | elif category == scripts:
232 | stale = []
233 | additional, failures = jedi_dump.dump_script_nodes(self.settings[sys_path], to_add)
234 |
235 | # Execute destructive operations now that we are done with
236 | # failure-prone operations
237 | self.settings[category] = decoded
238 |
239 | for name in to_delete:
240 | stale_node = self.script_nodes['python'].pop(name)
241 | stale.append(stale_node)
242 |
243 | self.script_nodes['python'].update(additional)
244 | self.failures['python'][category].update(failures)
245 |
246 | self.update_usage_search_locations('python')
247 |
248 | return (stale or additional, stale, additional.values())
249 |
250 | elif category == sys_path:
251 | self.settings[sys_path] = decoded
252 |
253 | platform = 'python'
254 | self.update_module_resolution_path(platform)
255 | self.make_platform_specific_nodes(platform) # depends on self.settings[sys_path]
256 |
257 | return (old != decoded, to_delete, to_add)
258 |
259 | elif category == bookmarks:
260 | self.settings[bookmarks] = decoded
261 | return (old != decoded, to_delete, to_add)
262 | else:
263 | raise NotImplementedError
264 |
265 | raise NotImplementedError
266 |
267 |
268 | def make_platform_specific_nodes(self, platform: str):
269 | if platform.lower().startswith('python'):
270 | from . import jedi_dump
271 |
272 | self.failures[platform] = {}
273 |
274 | self.module_nodes[platform], self.failures[platform][modules] = (
275 | jedi_dump.dump_module_nodes(self.settings[sys_path], self.settings[modules]))
276 |
277 | self.script_nodes[platform], self.failures[platform][scripts] = (
278 | jedi_dump.dump_script_nodes(self.settings[sys_path], self.settings[scripts]))
279 |
280 | self.update_usage_search_locations(platform)
281 |
282 |
283 | def update_usage_search_locations(self, platform: str):
284 | '''Update the places where usages are found
285 |
286 | Call this whenever you load new modules or scripts.
287 |
288 | '''
289 |
290 | if platform.lower().startswith('python'):
291 | from . import jedi_dump
292 |
293 | jedi_dump.JediCodeElementNode.usage_resolution_modules = (
294 | frozenset((nn.module_context for nn in
295 | tz.concatv(self.module_nodes[platform].values(),
296 | self.script_nodes[platform].values())
297 | if nn.code_element.path)))
298 |
299 | def update_module_resolution_path(self, platform: str):
300 | if platform.lower().startswith('python'):
301 | from . import jedi_dump
302 | jedi_dump.JediCodeElementNode.sys_path = [str(pp) for pp in self.settings[sys_path]]
303 |
--------------------------------------------------------------------------------
/call_map/qt_compatibility.py:
--------------------------------------------------------------------------------
1 | '''
2 | The qt_compatibility module is to make it easy to switch between different Qt/Python bridge implementations.
3 |
4 | '''
5 |
6 |
7 | from PyQt5 import QtCore, QtGui, QtWidgets, Qt
8 |
9 | # The following monkey patching already occurs when importing
10 | # qtconsole.pygments_highlighter, however that should not be relied upon.
11 | QtCore.Signal = QtCore.pyqtSignal
12 |
--------------------------------------------------------------------------------
/call_map/serialize.py:
--------------------------------------------------------------------------------
1 | from typing import Union, Dict, List, Any, Callable, Tuple
2 | from pathlib import Path
3 | import toolz
4 | from .core import CodeElement
5 | from .custom_typing import CheckableOptional, CheckableDict, CheckableList, CheckableTuple
6 |
7 | NoneType = type(None)
8 |
9 | py_type = 'py_type'
10 | value = 'value'
11 |
12 | encoding_fns = {}
13 | decoding_fns = {}
14 |
15 | class DecodeError(Exception):
16 | pass
17 |
18 |
19 | def register(py_type: Any, encoder: Callable, decoder: Callable):
20 | """Register JSON encoding name and JSON encoding/decoding functions"""
21 | encoding_fns[py_type] = encoder
22 | decoding_fns[py_type] = decoder
23 |
24 |
25 | def encode(type_spec, oo: Any):
26 | """Encode a Python object to JSON serializable form"""
27 |
28 | if isinstance(type_spec, CheckableDict):
29 | return {k: encode(type_spec.value_types[k], v) for k, v in oo.items()}
30 |
31 | elif isinstance(type_spec, CheckableOptional):
32 | if oo is not None:
33 | return encode(type_spec.nontrivial_type, oo)
34 | else:
35 | return None
36 |
37 | elif isinstance(type_spec, CheckableList):
38 | return [encode(type_spec.value_type, elt) for elt in oo]
39 |
40 | elif isinstance(type_spec, CheckableTuple):
41 | return tuple(encode(elt_type, elt) for elt_type, elt in zip(type_spec.value_types, oo))
42 |
43 | else:
44 | try:
45 | return encoding_fns[type_spec](oo)
46 | except KeyError:
47 | raise TypeError('Cannot encode {}'.format(repr(oo)))
48 |
49 |
50 | def decode(type_spec, oo: Union[Dict[str, Any], List, str, int, type(None)]):
51 | """Decode a Python object encoded by `encode`"""
52 | try:
53 | if isinstance(type_spec, CheckableDict):
54 | return {k: decode(type_spec.value_types[k], v) for k, v in oo.items()}
55 |
56 | elif isinstance(type_spec, CheckableOptional):
57 | if oo is not None:
58 | return decode(type_spec.nontrivial_type, oo)
59 | else:
60 | return None
61 |
62 | elif isinstance(type_spec, CheckableList):
63 | return [decode(type_spec.value_type, elt) for elt in oo]
64 |
65 | elif isinstance(type_spec, CheckableTuple):
66 | return tuple(decode(elt_type, elt) for elt_type, elt in zip(type_spec.value_types, oo))
67 |
68 | elif type_spec in decoding_fns:
69 | return decoding_fns[type_spec](oo)
70 |
71 | else:
72 | raise TypeError
73 |
74 | except (TypeError, AttributeError, KeyError) as err:
75 | raise DecodeError('Cannot decode `{}` as type {}; {}'.format(oo, type_spec, err))
76 |
77 |
78 |
79 | def encode_code_element(ce):
80 | return ce._asdict()
81 |
82 | def decode_code_element(encoded):
83 | ce1 = CodeElement(**encoded)
84 | path, line, column = ce1.call_pos
85 | return ce1._replace(call_pos=(path, tuple(line), tuple(column)),
86 | start_pos=tuple(ce1.start_pos),
87 | end_pos=tuple(ce1.end_pos))
88 |
89 |
90 | register(Path, encoder=str, decoder=Path)
91 | register(str, encoder=toolz.identity, decoder=toolz.identity)
92 | register(int, encoder=toolz.identity, decoder=toolz.identity)
93 | register(float, encoder=toolz.identity, decoder=toolz.identity)
94 | register(NoneType, encoder=toolz.identity, decoder=toolz.identity)
95 | register(CodeElement, encoder=encode_code_element, decoder=decode_code_element)
96 |
--------------------------------------------------------------------------------
/call_map/wheel_fix.py:
--------------------------------------------------------------------------------
1 | """
2 | Utilities for locking scroll orientation
3 |
4 | Normally you can scroll in any direction you want, but sometimes that is
5 | not desirable.
6 |
7 | The only function you should be using from this module is
8 | :func:`notify_wheel_event`. Use it from your custom notify method,
9 | for example::
10 |
11 | class MyQApplication(QtGui.QApplication):
12 | def notify(self, obj: QtCore.QObject, event: QtCore.QEvent):
13 | if (event.type() == QtCore.QEvent.Wheel):
14 | return wheel_fix.notify_wheel_event(self, obj, event)
15 | else:
16 | return super().notify(obj, event)
17 |
18 | """
19 |
20 | from PyQt5 import QtCore, QtGui, Qt, QtWidgets
21 |
22 | HORIZONTAL = 1
23 | VERTICAL = 2
24 |
25 | LOCK_INTERVAL = 200
26 |
27 | locked_orientation = None
28 |
29 | timer = QtCore.QTimer()
30 | timer.setInterval(LOCK_INTERVAL)
31 |
32 |
33 | def unlock():
34 | global locked_orientation
35 | locked_orientation = None
36 | timer.stop()
37 |
38 | timer.timeout.connect(unlock)
39 |
40 |
41 | def update_lock_orientation(event):
42 | global locked_orientation
43 |
44 | if locked_orientation is None:
45 | locked_orientation = event.orientation()
46 | else:
47 | timer.stop()
48 |
49 | timer.start()
50 |
51 |
52 | def notify_wheel_event(app: QtWidgets.QApplication,
53 | obj: QtCore.QObject,
54 | event: QtCore.QEvent):
55 | """Notify obj or obj.parent() based on orientation lock state"""
56 |
57 | update_lock_orientation(event)
58 |
59 | if (event.orientation() == locked_orientation
60 | and (type(obj) is not QtGui.QScrollBar
61 | or obj.orientation() == locked_orientation)):
62 |
63 | return super(type(app), app).notify(obj, event)
64 | else:
65 | return super(type(app), app).notify(obj.parent(), event)
66 |
--------------------------------------------------------------------------------
/dev_helper_tools/shell_tools.zsh:
--------------------------------------------------------------------------------
1 |
2 | function ipy_call_map {
3 | QT_API=pyqt5 ipython --gui=qt -im call_map -- --ipython $@
4 | }
5 |
--------------------------------------------------------------------------------
/docs/UI-clicked-on-function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/call_map/8f9b41260f654595e7520fade107d018ea011c0d/docs/UI-clicked-on-function.png
--------------------------------------------------------------------------------
/gpl-3.0.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/optional-requirements.txt:
--------------------------------------------------------------------------------
1 | # for highlighting
2 | qtconsole
3 |
4 | # for profiling
5 | profilehooks
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | import glob
3 | import os.path as osp
4 | import json as _json
5 | from pathlib import Path as _Path
6 |
7 | requirements = [
8 | #'jedi>=0.10.0, <=0.10.2',
9 | 'jedi==0.10.2',
10 | 'toolz', 'pygments', 'qtconsole']
11 |
12 | # If PyQt5 was installed using conda, pip will not recognize it
13 | # Therefore import it to see if it is installed.
14 | try:
15 | import PyQt5
16 | except ImportError:
17 | requirements += ['PyQt5']
18 |
19 | _package_info = (
20 | _json.loads(_Path(__file__).parent.joinpath('call_map', 'package_info.json').read_text()))
21 |
22 | setuptools.setup(
23 | name='call_map',
24 | version=_package_info['version'],
25 | description='A GUI for viewing call graphs in Python',
26 | author='Andy Lee',
27 | license='MIT',
28 | classifiers=[
29 | 'Programming Language :: Python :: 3.5',
30 | ],
31 |
32 | install_requires=requirements,
33 |
34 | entry_points={
35 | 'console_scripts': ['call_map=call_map.gui:main'],
36 | },
37 |
38 | package_data={
39 | '': ['*.png']
40 | },
41 |
42 | packages=setuptools.find_packages(exclude=['contrib', 'docs', 'tests*', 'scratch'])
43 | )
44 |
--------------------------------------------------------------------------------
/tests/load_test_modules.py:
--------------------------------------------------------------------------------
1 | import toolz as tz
2 |
3 | from call_map.core import UserScopeSettings, ScopeSettings, OrganizerNode
4 | from call_map.jedi_dump import make_scope_settings
5 | from call_map import project_settings_module
6 | from call_map.project_settings_module import Project
7 | from pathlib import Path
8 | from sys import path as runtime_sys_path
9 |
10 | test_modules_dir = Path(__file__).parent.joinpath('test_modules')
11 |
12 | user_scope_settings = UserScopeSettings(
13 | module_names=[],
14 | file_names=test_modules_dir.glob('*.py'),
15 | include_runtime_sys_path=True,
16 | add_to_sys_path=([str(test_modules_dir)] + runtime_sys_path),
17 | )
18 |
19 | scope_settings = make_scope_settings(is_new_project=True,
20 | saved_scope_settings=ScopeSettings([], [], []),
21 | user_scope_settings=user_scope_settings) # type: ScopeSettings
22 |
23 | project = Project(None)
24 |
25 | project.settings.update(
26 | {project_settings_module.modules: scope_settings.module_names,
27 | project_settings_module.scripts: scope_settings.scripts,
28 | project_settings_module.sys_path: scope_settings.effective_sys_path})
29 |
30 | project.make_platform_specific_nodes('python')
31 |
32 | root_node = OrganizerNode('Root', [],
33 | list(tz.concatv(project.module_nodes['python'].values(),
34 | project.script_nodes['python'].values())))
35 |
--------------------------------------------------------------------------------
/tests/test_cache.py:
--------------------------------------------------------------------------------
1 |
2 | from pathlib import Path
3 | from call_map.cache import read_text_cached
4 | import call_map.cache
5 | import random
6 |
7 | call_map.cache.TEXT_CACHE_SIZE = 10
8 |
9 | class FakePath:
10 | def __init__(self, name, text):
11 | self.name = name
12 | self.text = text
13 |
14 | def __hash__(self):
15 | return hash(self.name)
16 |
17 | def read_text(self):
18 | return self.text
19 |
20 |
21 | def test_read_text_cached():
22 |
23 | paths = [FakePath(str(ii), str(ii)) for ii in range(100)]
24 |
25 | for path in paths[:call_map.cache.TEXT_CACHE_SIZE]:
26 | assert read_text_cached(path) == path.text
27 |
28 | assert len(call_map.cache._text_cache) == call_map.cache.TEXT_CACHE_SIZE
29 |
30 | for ii in range(1000):
31 | path = random.choice(paths)
32 | assert read_text_cached(path) == path.text
33 | assert len(call_map.cache._text_cache) == call_map.cache.TEXT_CACHE_SIZE
34 |
--------------------------------------------------------------------------------
/tests/test_jedi_ast_tools.py:
--------------------------------------------------------------------------------
1 | import jedi
2 | import call_map.jedi_ast_tools as jat
3 | import toolz as tz
4 | import textwrap
5 |
6 |
7 | def test_get_called_functions():
8 |
9 | test_script = """
10 |
11 | import call_map.jedi_ast_tools as jat
12 |
13 |
14 | def thunk():
15 | print('hi')
16 |
17 |
18 | def ff(node):
19 | aa = jat.get_called_functions(node)
20 | thunk()
21 |
22 | """
23 |
24 | text_script = textwrap.dedent(test_script)
25 |
26 | definitions = jedi.api.names(source=test_script)
27 |
28 | def_ff = tz.first(filter(lambda x: x.name == 'ff', definitions))
29 | called_by_ff = list(jat.get_called_functions(def_ff._name.tree_name.get_definition().children[-1]))
30 |
31 | assert len(called_by_ff) == 2
32 | assert {name.value for role, name, ast_node, start_pos, end_pos in called_by_ff} == {'thunk', 'get_called_functions'}
33 |
--------------------------------------------------------------------------------
/tests/test_jedi_dump.py:
--------------------------------------------------------------------------------
1 | import toolz as tz
2 |
3 | from load_test_modules import root_node, test_modules_dir
4 | from call_map.config import user_config
5 |
6 | use_decorators_node = tz.first(node for node in root_node.children
7 | if node.code_element.name == 'use_decorators')
8 |
9 | use_comprehension_node = tz.first(node for node in root_node.children
10 | if node.code_element.name == 'use_comprehension')
11 |
12 | def test_decorator_child():
13 | user_config.session_overrides['EXPERIMENTAL_MODE'] = False
14 |
15 | children = list(use_decorators_node.children)
16 |
17 | assert children
18 |
19 | assert any(
20 | node.code_element.name == 'dec'
21 | and node.code_element.call_pos == (str(test_modules_dir.joinpath('use_decorators.py')), (7, 1), (7, 4))
22 | for node in children
23 | )
24 |
25 |
26 | def test_comprehension():
27 | user_config.session_overrides['EXPERIMENTAL_MODE'] = False
28 |
29 | nodes = [node for node in use_comprehension_node.children
30 | if node.code_element.name == 'ff']
31 |
32 | assert nodes
33 |
34 | ff_node = tz.first(nodes)
35 |
36 | fn_with_comprehension_node = tz.first(
37 | node for node in use_comprehension_node.children
38 | if node.code_element.name == 'fn_with_comprehension'
39 | )
40 |
41 | assert any(node.code_element.name == 'fn_with_comprehension' for node in ff_node.parents)
42 | assert any(node.code_element.name == 'ff' for node in fn_with_comprehension_node.children)
43 |
--------------------------------------------------------------------------------
/tests/test_modules/simple_test_package/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A simple import tree for testing
3 | """
4 |
5 |
--------------------------------------------------------------------------------
/tests/test_modules/simple_test_package/aa.py:
--------------------------------------------------------------------------------
1 | from . import bb
2 |
3 | def foo():
4 | bb.bar()
5 |
--------------------------------------------------------------------------------
/tests/test_modules/simple_test_package/bb.py:
--------------------------------------------------------------------------------
1 | def bar():
2 | pass
3 |
--------------------------------------------------------------------------------
/tests/test_modules/test_package_sibling_usage/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Tests whether jedi finds a usage of a function in another module in the same package
3 | on startup.
4 |
5 | The test is currently not automated.
6 | '''
7 |
--------------------------------------------------------------------------------
/tests/test_modules/test_package_sibling_usage/subpackage/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/call_map/8f9b41260f654595e7520fade107d018ea011c0d/tests/test_modules/test_package_sibling_usage/subpackage/__init__.py
--------------------------------------------------------------------------------
/tests/test_modules/test_package_sibling_usage/subpackage/test_sibling_usage.py:
--------------------------------------------------------------------------------
1 | from .. import test_sibling_used
2 |
3 | def uses_sibling():
4 | return test_sibling_used.used_function()
5 |
--------------------------------------------------------------------------------
/tests/test_modules/test_package_sibling_usage/test_sibling_used.py:
--------------------------------------------------------------------------------
1 |
2 | def used_function():
3 | return 1
4 |
--------------------------------------------------------------------------------
/tests/test_modules/use_comprehension.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | def ff(x):
4 | pass
5 |
6 | def fn_with_comprehension():
7 | return [ff(ii) for ii in range(10)]
8 |
--------------------------------------------------------------------------------
/tests/test_modules/use_decorators.py:
--------------------------------------------------------------------------------
1 |
2 | import functools
3 |
4 | def dec(f):
5 | pass
6 |
7 | @dec
8 | def ff():
9 | def gg():
10 | pass
11 | pass
12 |
--------------------------------------------------------------------------------
/tests/test_project.py:
--------------------------------------------------------------------------------
1 |
2 | from pathlib import Path
3 | from typing import Optional
4 | import toolz as tz
5 | import pytest
6 | import json
7 |
8 | import call_map.project_settings_module
9 | from call_map.gui import make_app
10 | from call_map.core import UserScopeSettings
11 | from call_map.config import user_config
12 |
13 |
14 | test_modules_dir = Path(__file__).parent.joinpath("test_modules")
15 |
16 |
17 | @pytest.fixture(scope='module')
18 | def testing_project_directory(tmpdir_factory):
19 | return Path(str(tmpdir_factory.mktemp('project_directory')))
20 |
21 |
22 | @pytest.fixture(scope='module')
23 | def second_project_directory(tmpdir_factory):
24 | return Path(str(tmpdir_factory.mktemp('project_directory')))
25 |
26 |
27 | @pytest.fixture(scope='module')
28 | def testing_ui(testing_project_directory):
29 | '''Returns ui_toplevel module for the app'''
30 |
31 | module_names = ['simple_test_package']
32 | #module_names = ['toolz']
33 | user_scope_settings = UserScopeSettings(
34 | module_names=module_names,
35 | file_names=[],
36 | include_runtime_sys_path=True,
37 | add_to_sys_path=[test_modules_dir])
38 | ui_toplevel = make_app(user_scope_settings,
39 | project_directory=testing_project_directory,
40 | enable_ipython_support=True,
41 | show_gui=False)
42 |
43 | return ui_toplevel
44 |
45 |
46 | def iterListWidget(ll):
47 | for ii in range(ll.count()):
48 | yield ll.item(ii)
49 |
50 |
51 | def append_to_list_settings_widget(list_settings_widget, item_name):
52 | textEdit = list_settings_widget.textEdit
53 | jj = json.loads(textEdit.toPlainText())
54 | jj.append(item_name)
55 | textEdit.setPlainText(json.dumps(jj))
56 | list_settings_widget.commit()
57 |
58 |
59 | def test_add_module(testing_ui):
60 | append_to_list_settings_widget(testing_ui.settings_widget.module_settings_widget,
61 | 'json')
62 | assert 'json' in testing_ui.project.settings[call_map.project_settings_module.modules]
63 |
64 |
65 | def test_add_bookmark(testing_ui):
66 | testing_ui.map_widget.callLists
67 |
68 | map_widget = testing_ui.map_widget
69 |
70 | user_config.session_overrides['MULTITHREADING'] = False
71 | user_config.session_overrides['EXPERIMENTAL_MODE'] = False
72 |
73 | # Step 1: Choose a path and bookmark it.
74 | for ii, target_name in enumerate(['simple_test_package', 'bb', 'bar', 'foo']):
75 | for item in iterListWidget(map_widget.callLists[ii]):
76 | if item.node.code_element.name == target_name:
77 | map_widget.callLists[ii].setCurrentItem(item)
78 |
79 | bookmarked_node_path = list(map_widget.node_path())
80 | bookmarks_widget = testing_ui.settings_widget.bookmarks_widget
81 | bookmarks_widget.addBookmark()
82 |
83 | # Step 2: Go down a different path.
84 | for ii, target_name in enumerate(['simple_test_package', 'aa']):
85 | for item in iterListWidget(map_widget.callLists[ii]):
86 | if item.node.code_element.name == target_name:
87 | map_widget.callLists[ii].setCurrentItem(item)
88 |
89 | assert not all(aa.code_element == bb.code_element
90 | for aa, bb in
91 | zip(map_widget.node_path(), bookmarked_node_path))
92 |
93 | # Step 3: Revisit bookmark
94 | assert bookmarks_widget.listWidget.count() == 1
95 | bookmarks_widget.listWidget.setCurrentRow(0)
96 | bookmarks_widget.visitBookmark()
97 |
98 | assert all(aa.code_element == bb.code_element
99 | for aa, bb in
100 | zip(map_widget.node_path(), bookmarked_node_path))
101 |
102 |
103 |
104 | def test_change_project_directory(testing_ui, testing_project_directory,
105 | second_project_directory):
106 |
107 | def change_project_directory(project_directory):
108 | textEdit = testing_ui.settings_widget.project_settings_widget.textEdit
109 | jj = json.loads(textEdit.toPlainText())
110 | jj['project_directory'] = str(project_directory)
111 | textEdit.setPlainText(json.dumps(jj))
112 | testing_ui.settings_widget.project_settings_widget.commit()
113 |
114 | original_project = call_map.project_settings_module.Project(testing_project_directory)
115 |
116 | original_project.update_settings(original_project.load_from_persistent_storage())
117 |
118 | assert original_project.settings[call_map.project_settings_module.modules]
119 |
120 | change_project_directory(second_project_directory)
121 | second_project = call_map.project_settings_module.Project(second_project_directory)
122 | second_project.update_settings(second_project.load_from_persistent_storage())
123 |
124 | assert (testing_ui.settings_widget.project_settings_widget.project.project_directory
125 | == second_project_directory)
126 |
127 | assert tz.assoc_in(
128 | original_project.settings,
129 | [call_map.project_settings_module.project_settings, 'project_directory'],
130 | second_project.project_directory
131 | ) == second_project.settings
132 |
133 | change_project_directory(testing_project_directory)
134 |
135 |
136 | def test_change_sys_path():
137 | pass
138 |
139 |
140 | def test_change_scripts(testing_ui):
141 | append_to_list_settings_widget(testing_ui.settings_widget.script_settings_widget,
142 | __file__)
143 | assert Path(__file__) in testing_ui.project.settings[call_map.project_settings_module.scripts]
--------------------------------------------------------------------------------
/tests/test_ui.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pathlib import Path
3 | from concurrent.futures import wait
4 |
5 | from call_map.gui import make_app
6 | from call_map.core import UserScopeSettings, ScopeSettings, OrganizerNode, CodeElement
7 | from call_map.config import user_config
8 |
9 | test_modules_dir = Path(__file__).parent.joinpath("test_modules")
10 |
11 |
12 | def create_testing_app(project_directory: Optional[str]):
13 | module_names = ['simple_test_package']
14 | #module_names = ['toolz']
15 | user_scope_settings = UserScopeSettings(
16 | module_names=module_names,
17 | file_names=[],
18 | include_runtime_sys_path=True,
19 | add_to_sys_path=[test_modules_dir])
20 | ui_toplevel = make_app(user_scope_settings, project_directory=project_directory, enable_ipython_support=True)
21 |
22 | return ui_toplevel
23 |
24 |
25 | def iterListWidget(ll):
26 | for ii in range(ll.count()):
27 | yield ll.item(ii)
28 |
29 |
30 | def test_definition_resolution():
31 | # create application, open on
32 | ui_toplevel = create_testing_app(project_directory=None)
33 |
34 | map_widget = ui_toplevel.map_widget
35 |
36 | user_config.session_overrides['MULTITHREADING'] = False
37 | user_config.session_overrides['EXPERIMENTAL_MODE'] = False
38 |
39 | for ii, target_name in enumerate(['simple_test_package', 'aa', 'foo', 'bar']):
40 | for item in iterListWidget(map_widget.callLists[ii]):
41 | if item.node.code_element.name == target_name:
42 | map_widget.callLists[ii].setCurrentItem(item)
43 | break
44 | else:
45 | raise ValueError('Could not find {}'.format(target_name))
46 |
47 | # test that the 'bar' node has the correct properties. TODO: test more properties
48 | assert map_widget.callLists[3].currentItem().node.code_element.module == 'bb'
49 |
50 | #ll = map_widget.callLists[0]
51 | #print(ll.node)
52 | #print(ll.node.children)
53 |
54 | #print(list(iterListWidget(ui_toplevel.map_widget.callLists[0]))[0])
55 | #print(list(ui_toplevel.map_widget.node_path()))
56 |
57 |
58 | def test_usages_resolution():
59 | # create application, open on
60 | ui_toplevel = create_testing_app(project_directory=None)
61 |
62 | map_widget = ui_toplevel.map_widget
63 |
64 | user_config.session_overrides['MULTITHREADING'] = False
65 | user_config.session_overrides['EXPERIMENTAL_MODE'] = False
66 |
67 | for ii, target_name in enumerate(['simple_test_package', 'bb', 'bar', 'foo']):
68 | for item in iterListWidget(map_widget.callLists[ii]):
69 | if item.node.code_element.name == target_name:
70 | map_widget.callLists[ii].setCurrentItem(item)
71 |
72 | # test that we found the usage of bar in the function `aa.foo`
73 | assert map_widget.callLists[3].currentItem().node.code_element.role == 'parent'
74 | assert map_widget.callLists[3].currentItem().node.code_element.type == 'function'
75 | assert map_widget.callLists[3].currentItem().node.code_element.module == 'aa'
76 |
77 |
78 | def test_definition_resolution_with_script():
79 | # TODO: add a script node, test definition resolution.
80 | pass
81 |
82 |
83 | def test_usages_resolution_with_script():
84 | # TODO: add a script node, test usages resolution.
85 | pass
86 |
--------------------------------------------------------------------------------