├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── pyq
├── __init__.py
├── astmatch.py
└── pyq.py
├── setup.py
├── sizzle
├── __init__.py
├── match.py
└── selector.py
├── test_cmd.py
├── test_pyq.py
├── test_sizzle.py
└── testfiles
├── assign.py
├── attrs.py
├── calls.py
├── classes.py
├── cmd
├── .test_hidden_dir
│ └── foo.py
├── cmd.py
├── file2.py
├── ignoredir
│ ├── ignoredir2
│ │ └── test.py
│ └── test.py
└── notpyfile.txt
├── ids.py
└── imports.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 |
55 | # Sphinx documentation
56 | docs/_build/
57 |
58 | # PyBuilder
59 | target/
60 |
61 | #Ipython Notebook
62 | .ipynb_checkpoints
63 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Current
4 |
5 | ### New features
6 |
7 | * Added `--expand` option that allows showing multiples matches in the same
8 | line
9 |
10 | ## 0.0.6 / 2016-02-26
11 |
12 | ### New features
13 |
14 | * Added type `attr` (eg. `attr#foo` will match `a.foo.bar` and `a.bar.foo`)
15 | * Added support for wildcards
16 | * Added special attribute `full` to match imports: `import[full=x.z]` will
17 | match `from x import y as z`
18 | * Added ability to match calls with certain `arg` or `kwarg` (eg.
19 | `call[arg=foo]` will match `bar(x, y, foo)`; `[kwarg=bar]` will match
20 | `foo(bar=1, z=2)`
21 | * Added `--ignore-dir` and `--no-recurse` options
22 |
23 | ### Bug fixed
24 |
25 | * Fixed bug when `node.body` is not an iterator (eg. lambdas)
26 |
27 |
28 | ## 0.0.5 / 2016-02-11
29 |
30 | ### New features
31 |
32 | * Added CHANGELOG.md
33 | * Added support for the type `assign` (eg. `assign#foo` will match `foo = 1`)
34 | * Changed `extends()` pseudo-selector to receive a selector as argument
35 | * Added support for the type `call` (eg. `call#foo` will match `foo(1)`)
36 |
37 |
38 | ### Bug fixes
39 |
40 | * Fixed bug when using `:extends()` without the type `class` specified
41 | * Fixed bug when trying to match `#name` with a node that doesn't have a name
42 | attribute
43 |
44 |
45 | ## 0.0.4 / 2016-02-10
46 |
47 | * Added `-l/--files` option to print only files names
48 |
49 |
50 | ## 0.0.3 / 2016-02-09
51 |
52 | * Added support for Python 2
53 |
54 |
55 | ## 0.0.2 / 2016-02-08
56 |
57 | * Changed package name from pyq to pyqtool, to avoid conflicts w/ existing
58 | packages
59 |
60 |
61 | ## 0.0.1 / 2016-02-07
62 |
63 | * Initial release
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Caio Ariede
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pyq
2 |
3 | A command-line tool to search for Python code using jQuery-like selectors
4 |
5 | [](https://badge.fury.io/py/pyqtool)
6 |
7 |
8 | ## Installation
9 |
10 | pip install pyqtool
11 |
12 | **Notice:** As the tool is still under heavy development, you may see that some features are not yet available in the version distributed over PyPI. If you want to have a fresh taste, you can get it directly from source:
13 |
14 | pip install https://github.com/caioariede/pyq/archive/master.zip -U
15 |
16 | Please report any possible issues, we expect the master branch to be stable.
17 |
18 |
19 | ## Usage
20 |
21 | Usage: pyq3 [OPTIONS] SELECTOR [PATH]...
22 |
23 | Options:
24 | -l / --files Only print filenames containing matches.
25 | --ignore-dir TEXT Ignore directory.
26 | -n / --no-recurse No descending into subdirectories.
27 | -e / --expand Show multiple matches in the same line.
28 | --help Show this message and exit.
29 |
30 | The executable name will vary depending on the Python version: `pyq2` `pyq3`
31 |
32 |
33 | ## Available selectors
34 |
35 | ##### Type selectors
36 |
37 | | Name | Attributes | Additional notes |
38 | | ------ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
39 | | class | class `name` | |
40 | | def | def `name` | |
41 | | import | import `name`
import `name` as `name`
from `from` import `name` | It's also possible to match the full import using
the special attribute `full` |
42 | | assign | `name` [, `name` ...] = value | |
43 | | call | `name`(`arg`, `arg`, ..., `kwarg`=, `kwarg`=, ...) | |
44 | | attr | foo.`name`.`name` | |
45 |
46 | ##### ID/Name selector
47 |
48 | | Syntax | Applied to |
49 | | -------- | ---------------------------------------- |
50 | | #`name` | `class`, `def`, `assign`, `call`, `attr` |
51 |
52 |
53 | #### Attribute selectors
54 |
55 | | Syntax | Description |
56 | | ----------------- | ------------------------------------------ |
57 | | [`name`=`value`] | Attribute `name` is equal to `value` |
58 | | [`name`!=`value`] | Attribute `name` is not equal to `value` |
59 | | [`name`*=`value`] | Attribute `name` contains `value` |
60 | | [`name`^=`value`] | Attribute `name` starts with `value` |
61 | | [`name`$=`value`] | Attribute `name` endswith `value` |
62 |
63 |
64 | #### Pseudo selectors
65 |
66 | | Syntax | Applies to | Description |
67 | | --------------------- | ----------------- | -------------------------------------------------- |
68 | | :extends(`selector`) | `class` | Selects classes that its bases matches `selector` |
69 | | :has(`selector`) | _all_ | Selects everything that its body match `selector` |
70 | | :not(`selector`) | _all_ | Selects everything that do not match `selector` |
71 |
72 | #### Combinators
73 |
74 | | Syntax | Description |
75 | | --------------------- | -------------------------------------- |
76 | | `parent` > `child` | Select direct `child` from `parent` |
77 | | `parent` `descendant` | Selects all `descendant` from `parent` |
78 |
79 |
80 | ## Examples
81 |
82 | Search for classes that extends the `IntegerField` class:
83 |
84 | ```python
85 | ❯ pyq3 'class:extends(#IntegerField)' django/forms
86 | django/forms/fields.py:278 class FloatField(IntegerField):
87 | django/forms/fields.py:315 class DecimalField(IntegerField):
88 | ```
89 |
90 | Search for classes with the name `FloatField`:
91 |
92 | ```python
93 | ❯ pyq3 'class[name=FloatField]' django/forms
94 | django/forms/fields.py:278 class FloatField(IntegerField):
95 | ```
96 |
97 | Search for methods under the `FloatField` class:
98 |
99 | ```python
100 | ❯ pyq3 'class[name=FloatField] > def' django/forms
101 | django/forms/fields.py:283 def to_python(self, value):
102 | django/forms/fields.py:299 def validate(self, value):
103 | django/forms/fields.py:308 def widget_attrs(self, widget):
104 | ```
105 |
106 | Search for methods whose name starts with `to` under the `FloatField` class:
107 |
108 | ```python
109 | ❯ pyq3 'class[name=FloatField] > def[name^=to]' django/forms
110 | django/forms/fields.py:283 def to_python(self, value):
111 | ```
112 |
113 | Search for import statements importing `Counter`:
114 |
115 | ```python
116 | ❯ pyq3 'import[from=collections][name=Counter]' django/
117 | django/apps/registry.py:5 from collections import Counter, OrderedDict, defaultdict
118 | django/template/utils.py:3 from collections import Counter, OrderedDict
119 | django/test/testcases.py:14 from collections import Counter
120 | ...
121 | ```
122 |
123 | Search for classes without methods:
124 |
125 | ```python
126 | ❯ pyq3 'class:not(:has(> def))' django/core
127 | django/core/exceptions.py:8 class FieldDoesNotExist(Exception):
128 | django/core/exceptions.py:13 class DjangoRuntimeWarning(RuntimeWarning):
129 | django/core/exceptions.py:17 class AppRegistryNotReady(Exception):
130 | ...
131 | ```
132 |
--------------------------------------------------------------------------------
/pyq/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caioariede/pyq/29248ab285dfd43c5a634dc895021eacaf61d71f/pyq/__init__.py
--------------------------------------------------------------------------------
/pyq/astmatch.py:
--------------------------------------------------------------------------------
1 | from sizzle.match import MatchEngine
2 |
3 | import ast
4 | import astor
5 |
6 |
7 | class ASTMatchEngine(MatchEngine):
8 | def __init__(self):
9 | super(ASTMatchEngine, self).__init__()
10 | self.register_pseudo('extends', self.pseudo_extends)
11 |
12 | def match(self, selector, filename):
13 | module = astor.parsefile(filename)
14 | for match in super(ASTMatchEngine, self).match(selector, module.body):
15 | lineno = match.lineno
16 | if isinstance(match, (ast.ClassDef, ast.FunctionDef)):
17 | for d in match.decorator_list:
18 | lineno += 1
19 | yield match, lineno
20 |
21 | @staticmethod
22 | def pseudo_extends(matcher, node, value):
23 | if not isinstance(node, ast.ClassDef):
24 | return False
25 |
26 | if not value:
27 | return node.bases == []
28 |
29 | bases = node.bases
30 | selectors = value.split(',')
31 |
32 | for selector in selectors:
33 | matches = matcher.match_data(
34 | matcher.parse_selector(selector)[0], bases)
35 | if any(matches):
36 | return True
37 |
38 | def match_type(self, typ, node):
39 | if typ == 'class':
40 | return isinstance(node, ast.ClassDef)
41 |
42 | if typ == 'def':
43 | return isinstance(node, ast.FunctionDef)
44 |
45 | if typ == 'import':
46 | return isinstance(node, (ast.Import, ast.ImportFrom))
47 |
48 | if typ == 'assign':
49 | return isinstance(node, ast.Assign)
50 |
51 | if typ == 'attr':
52 | return isinstance(node, ast.Attribute)
53 |
54 | if typ == 'call':
55 | if isinstance(node, ast.Call):
56 | return True
57 |
58 | # Python 2.x compatibility
59 | return hasattr(ast, 'Print') and isinstance(node, ast.Print)
60 |
61 | def match_id(self, id_, node):
62 | if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
63 | return node.name == id_
64 |
65 | if isinstance(node, ast.Name):
66 | return node.id == id_
67 |
68 | if isinstance(node, ast.Attribute):
69 | return node.attr == id_
70 |
71 | if isinstance(node, ast.Assign):
72 | for target in node.targets:
73 | if hasattr(target, 'id'):
74 | if target.id == id_:
75 | return True
76 | if hasattr(target, 'elts'):
77 | if id_ in self._extract_names_from_tuple(target):
78 | return True
79 | elif isinstance(target, ast.Subscript):
80 | if hasattr(target.value, 'id'):
81 | if target.value.id == id_:
82 | return True
83 |
84 | if isinstance(node, ast.Call):
85 | if isinstance(node.func, ast.Name) and node.func.id == id_:
86 | return True
87 |
88 | if id_ == 'print' \
89 | and hasattr(ast, 'Print') and isinstance(node, ast.Print):
90 | # Python 2.x compatibility
91 | return True
92 |
93 | def match_attr(self, lft, op, rgt, node):
94 | values = []
95 |
96 | if lft == 'from':
97 | if isinstance(node, ast.ImportFrom) and node.module:
98 | values.append(node.module)
99 |
100 | elif lft == 'full':
101 | if isinstance(node, (ast.Import, ast.ImportFrom)):
102 | module = ''
103 | if isinstance(node, ast.ImportFrom):
104 | if node.module:
105 | module = node.module + '.'
106 |
107 | for n in node.names:
108 | values.append(module + n.name)
109 | if n.asname:
110 | values.append(module + n.asname)
111 |
112 | elif lft == 'name':
113 | if isinstance(node, (ast.Import, ast.ImportFrom)):
114 | for alias in node.names:
115 | if alias.asname:
116 | values.append(alias.asname)
117 | values.append(alias.name)
118 |
119 | elif isinstance(node, ast.Call):
120 | if hasattr(node.func, 'id'):
121 | values.append(node.func.id)
122 |
123 | elif hasattr(ast, 'Print') and isinstance(node, ast.Print):
124 | values.append('print')
125 |
126 | elif isinstance(node, ast.Assign):
127 | for target in node.targets:
128 | if hasattr(target, 'id'):
129 | values.append(target.id)
130 | elif hasattr(target, 'elts'):
131 | values.extend(self._extract_names_from_tuple(target))
132 | elif isinstance(target, ast.Subscript):
133 | if hasattr(target.value, 'id'):
134 | values.append(target.value.id)
135 |
136 | elif hasattr(node, lft):
137 | values.append(getattr(node, lft))
138 |
139 | elif lft in ('kwarg', 'arg'):
140 | if isinstance(node, ast.Call):
141 | if lft == 'kwarg':
142 | values = [kw.arg for kw in node.keywords]
143 | elif lft == 'arg':
144 | values = [arg.id for arg in node.args]
145 |
146 | if op == '=':
147 | return any(value == rgt for value in values)
148 |
149 | if op == '!=':
150 | return any(value != rgt for value in values)
151 |
152 | if op == '*=':
153 | return any(rgt in value for value in values)
154 |
155 | if op == '^=':
156 | return any(value.startswith(rgt) for value in values)
157 |
158 | if op == '$=':
159 | return any(value.endswith(rgt) for value in values)
160 |
161 | raise Exception('Attribute operator {} not implemented'.format(op))
162 |
163 | def iter_data(self, data):
164 | for node in data:
165 | for n in self.iter_node(node):
166 | yield n
167 |
168 | def iter_node(self, node):
169 | silence = (ast.Expr,)
170 |
171 | if not isinstance(node, silence):
172 | try:
173 | body = node.body
174 |
175 | # check if is iterable
176 | list(body)
177 |
178 | except TypeError:
179 | body = [node.body]
180 |
181 | except AttributeError:
182 | body = None
183 |
184 | yield node, body
185 |
186 | for attr in ('value', 'func', 'right', 'left'):
187 | if hasattr(node, attr):
188 | value = getattr(node, attr)
189 | # reversed is used here so matches are returned in the
190 | # sequence they are read, eg.: foo.bar.bang
191 | for n in reversed(list(self.iter_node(value))):
192 | yield n
193 |
194 | @classmethod
195 | def _extract_names_from_tuple(cls, tupl):
196 | r = []
197 | for item in tupl.elts:
198 | if hasattr(item, 'elts'):
199 | r.extend(cls._extract_names_from_tuple(item))
200 | else:
201 | r.append(item.id)
202 | return r
203 |
--------------------------------------------------------------------------------
/pyq/pyq.py:
--------------------------------------------------------------------------------
1 | import click
2 | import os
3 |
4 | from .astmatch import ASTMatchEngine
5 | from pygments import highlight
6 | from pygments.lexers.python import PythonLexer
7 | from pygments.formatters.terminal import TerminalFormatter
8 |
9 |
10 | @click.command()
11 | @click.argument('selector')
12 | @click.option('-l/--files', is_flag=True,
13 | help='Only print filenames containing matches.')
14 | @click.option('--ignore-dir', multiple=True,
15 | help='Ignore directory.')
16 | @click.option('-n/--no-recurse', is_flag=True, default=False,
17 | help='No descending into subdirectories.')
18 | @click.option('-e/--expand', is_flag=True, default=False,
19 | help='Show multiple matches in the same line.')
20 | @click.argument('path', nargs=-1)
21 | @click.pass_context
22 | def main(ctx, selector, path, **opts):
23 | m = ASTMatchEngine()
24 |
25 | if len(path) == 0:
26 | path = ['.']
27 |
28 | ignore_dir = (opts['ignore_dir'], not opts['n'])
29 | for fn in walk_files(ctx, path, ignore_dir):
30 | if fn.endswith('.py'):
31 | display_matches(m, selector, os.path.relpath(fn), opts)
32 |
33 |
34 | def walk_files(ctx, paths, ignore_dir):
35 | for i, p in enumerate(paths):
36 | p = click.format_filename(p)
37 |
38 | if p == '.' or os.path.isdir(p):
39 | for root, dirs, files in os.walk(p):
40 | if is_dir_ignored(root.lstrip('./'), *ignore_dir):
41 | continue
42 | for fn in files:
43 | yield os.path.join(root, fn)
44 |
45 | elif os.path.exists(p):
46 | yield p
47 |
48 | elif i == 0:
49 | ctx.fail('{}: No such file or directory'.format(p))
50 | break
51 |
52 |
53 | def display_matches(m, selector, filename, opts):
54 | matches = matching_lines(m.match(selector, filename), filename)
55 |
56 | if opts.get('l'):
57 | files = {}
58 | for line, no, _ in matches:
59 | if opts.get('l'):
60 | if filename not in files:
61 | click.echo(filename)
62 | # do not repeat files
63 | files[filename] = True
64 |
65 | else:
66 | lines = {}
67 | for line, no, col in matches:
68 | text = highlight(line.strip(), PythonLexer(), TerminalFormatter())
69 | if not opts['e']:
70 | if no not in lines:
71 | lines[no] = True
72 | click.echo('{}:{} {}'.format(filename, no, text),
73 | nl=False)
74 | else:
75 | click.echo('{}:{}:{} {}'.format(filename, no, col, text),
76 | nl=False)
77 |
78 |
79 | def matching_lines(matches, filename):
80 | fp = None
81 | for match, lineno in matches:
82 | if fp is None:
83 | fp = open(filename, 'rb')
84 | else:
85 | fp.seek(0)
86 |
87 | i = 1
88 | while True:
89 | line = fp.readline()
90 |
91 | if not line:
92 | break
93 |
94 | if i == lineno:
95 | text = line.decode('utf-8')
96 | yield text, lineno, match.col_offset
97 | break
98 |
99 | i += 1
100 |
101 | if fp is not None:
102 | fp.close()
103 |
104 |
105 | def is_dir_ignored(path, ignore_dir, recurse):
106 | path = path.split(os.sep)
107 | if not recurse:
108 | return path[0] in ignore_dir
109 | for p in path:
110 | if p in ignore_dir:
111 | return True
112 | return False
113 |
114 |
115 | if __name__ == '__main__':
116 | main()
117 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | import sys
4 |
5 |
6 | VERSION = '0.0.6'
7 | PYVERSION = sys.version_info.major
8 |
9 |
10 | setup(
11 | name='pyqtool',
12 | version=VERSION,
13 | description="Search Python code using jQuery-like selectors",
14 | author="Caio Ariede",
15 | author_email="caio.ariede@gmail.com",
16 | url="http://github.com/caioariede/pyq",
17 | license="MIT",
18 | zip_safe=False,
19 | platforms=["any"],
20 | packages=['pyq', 'sizzle'],
21 | entry_points={
22 | 'console_scripts': ['pyq{} = pyq.pyq:main'.format(PYVERSION)],
23 | },
24 | classifiers=[
25 | "Intended Audience :: Developers",
26 | "Operating System :: OS Independent",
27 | "License :: OSI Approved :: MIT License",
28 | "Programming Language :: Python",
29 | "Programming Language :: Python :: 3",
30 | "Programming Language :: Python :: 3.4",
31 | ],
32 | include_package_data=True,
33 | install_requires=[
34 | 'click==6.2',
35 | 'Pygments==2.1',
36 | 'regex==2016.1.10',
37 | 'astor==0.5',
38 | ]
39 | )
40 |
--------------------------------------------------------------------------------
/sizzle/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caioariede/pyq/29248ab285dfd43c5a634dc895021eacaf61d71f/sizzle/__init__.py
--------------------------------------------------------------------------------
/sizzle/match.py:
--------------------------------------------------------------------------------
1 | from .selector import Selector
2 |
3 |
4 | class MatchEngine(object):
5 | pseudo_fns = {}
6 | selector_class = Selector
7 |
8 | def __init__(self):
9 | self.register_pseudo('not', self.pseudo_not)
10 | self.register_pseudo('has', self.pseudo_has)
11 |
12 | def register_pseudo(self, name, fn):
13 | self.pseudo_fns[name] = fn
14 |
15 | @staticmethod
16 | def pseudo_not(matcher, node, value):
17 | return not matcher.match_node(matcher.parse_selector(value)[0], node)
18 |
19 | @staticmethod
20 | def pseudo_has(matcher, node, value):
21 | for node, body in matcher.iter_data([node]):
22 | if body:
23 | return any(
24 | matcher.match_data(matcher.parse_selector(value)[0], body))
25 |
26 | def parse_selector(self, selector):
27 | return self.selector_class.parse(selector)
28 |
29 | def match(self, selector, data):
30 | selectors = self.parse_selector(selector)
31 | nodeids = {}
32 | for selector in selectors:
33 | for node in self.match_data(selector, data):
34 | nodeid = id(node)
35 | if nodeid not in nodeids:
36 | nodeids[nodeid] = None
37 | yield node
38 |
39 | def match_data(self, selector, data):
40 | for node, body in self._iter_data(data):
41 | match = self.match_node(selector, node)
42 |
43 | if match:
44 | next_selector = selector.next_selector
45 | if next_selector:
46 | if body:
47 | for node in self.match_data(next_selector, body):
48 | yield node
49 | else:
50 | yield node
51 |
52 | if body and not selector.combinator == self.selector_class.CHILD:
53 | for node in self.match_data(selector, body):
54 | yield node
55 |
56 | def match_node(self, selector, node):
57 | match = all(self.match_rules(selector, node))
58 |
59 | if match and selector.attrs:
60 | match &= all(self.match_attrs(selector, node))
61 |
62 | if match and selector.pseudos:
63 | match &= all(self.match_pseudos(selector, node))
64 |
65 | return match
66 |
67 | def match_rules(self, selector, node):
68 | if selector.typ:
69 | yield self.match_type(selector.typ, node)
70 |
71 | if selector.id_:
72 | yield self.match_id(selector.id_, node)
73 |
74 | def match_attrs(self, selector, node):
75 | for a in selector.attrs:
76 | lft, op, rgt = a
77 | yield self.match_attr(lft, op, rgt, node)
78 |
79 | def match_pseudos(self, selector, d):
80 | for p in selector.pseudos:
81 | name, value = p
82 | if name not in self.pseudo_fns:
83 | raise Exception('Selector not implemented: {}'.format(name))
84 | yield self.pseudo_fns[name](self, d, value)
85 |
86 | def _iter_data(self, data):
87 | for tupl in self.iter_data(data):
88 | if len(tupl) != 2:
89 | raise Exception(
90 | 'The iter_data method must yield pair tuples containing '
91 | 'the node and its body (empty if not available)')
92 | yield tupl
93 |
94 | def match_type(self, typ, node):
95 | raise NotImplementedError
96 |
97 | def match_id(self, id_, node):
98 | raise NotImplementedError
99 |
100 | def match_attr(self, lft, op, rgt, no):
101 | raise NotImplementedError
102 |
103 | def iter_data(self, data):
104 | raise NotImplementedError
105 |
--------------------------------------------------------------------------------
/sizzle/selector.py:
--------------------------------------------------------------------------------
1 | import regex
2 |
3 | from collections import namedtuple
4 |
5 |
6 | Attr = namedtuple('Attr', 'lft op rgt')
7 | Pseudo = namedtuple('Pseudo', 'name value')
8 |
9 |
10 | class Selector(object):
11 | DESCENDANT = ' '
12 | CHILD = '>'
13 | SIBLING = '~'
14 | ADJACENT = '+'
15 | NOT_SET = None
16 |
17 | class RE(object):
18 | id = r'_?[A-Za-z0-9_]+|_'
19 | ws = r'[\x20\t\r\n\f]*'
20 | comma = '^{ws},{ws}'.format(ws=ws)
21 | combinator = r'^{ws}([>+~ ]){ws}'.format(ws=ws)
22 |
23 | type_selector = '({id})'.format(id=id)
24 | id_selector = '#({id})'.format(id=id)
25 | class_selector = r'\.(' + id + ')'
26 | pseudo_selector = r'(:({id})\(([^()]+|(?1)?)\))'.format(id=id)
27 | attr_selector = r'\[{ws}({id}){ws}([*^$|!~]?=)(.*?)\]'.format(
28 | id=id, ws=ws)
29 |
30 | selector = '(?:(?:{typ})?({id}|{cls}|{pseudo}|{attr})+|{typ})'.format(
31 | typ=id, id=id_selector, cls=class_selector, pseudo=pseudo_selector,
32 | attr=attr_selector)
33 |
34 | def __init__(self, name, combinator=None):
35 | self.name = name
36 | self.combinator = combinator
37 | self.next_selector = None
38 |
39 | selector_patterns = {
40 | 'types': self.RE.type_selector,
41 | 'ids': self.RE.id_selector,
42 | 'classes': self.RE.class_selector,
43 | 'pseudos': self.RE.pseudo_selector,
44 | 'attrs': self.RE.attr_selector,
45 | }
46 |
47 | matches = {}
48 |
49 | while True:
50 | pattern_matched = False
51 | for key, pattern in selector_patterns.items():
52 | match = regex.search(r'^{}'.format(pattern), name)
53 | if match:
54 | i, pos = match.span()
55 | if key not in matches:
56 | matches[key] = []
57 | matches[key].append(match.groups())
58 | name = name[pos:]
59 | pattern_matched = True
60 | if not pattern_matched:
61 | break
62 |
63 | self.typ = None
64 | for types in matches.pop('types', []):
65 | self.typ = types[0]
66 |
67 | self.id_ = None
68 | for ids in matches.pop('ids', []):
69 | self.id_ = ids[0]
70 |
71 | self.classes = [a[0] for a in matches.pop('classes', [])]
72 |
73 | self.attrs = [
74 | Attr(l, o, r.strip())
75 | for l, o, r in matches.pop('attrs', [])
76 | ]
77 | self.pseudos = [
78 | Pseudo(*a[1:])
79 | for a in matches.pop('pseudos', [])
80 | ]
81 |
82 | def __repr__(self):
83 | return 'Selector <{}>'.format(self.name)
84 |
85 | @classmethod
86 | def parse(cls, string):
87 | selectors = []
88 |
89 | combinator = None
90 | prev_selector = None
91 |
92 | while True:
93 | match = regex.search(cls.RE.comma, string)
94 | if match:
95 | # skip comma
96 | _, pos = match.span()
97 | string = string[pos:]
98 | continue
99 |
100 | match = regex.search(cls.RE.combinator, string)
101 | if match:
102 | _, pos = match.span()
103 | combinator = string[:pos].strip()
104 | string = string[pos:]
105 | else:
106 | combinator = None
107 |
108 | match = regex.search(cls.RE.selector, string)
109 | if match:
110 | _, pos = match.span()
111 | seltext = string[:pos]
112 | string = string[pos:]
113 | selector = cls(seltext, combinator=combinator)
114 | if combinator is not None and prev_selector:
115 | prev_selector.next_selector = prev_selector = selector
116 | else:
117 | prev_selector = selector
118 | selectors.append(selector)
119 | continue
120 |
121 | break
122 |
123 | return selectors
124 |
--------------------------------------------------------------------------------
/test_cmd.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | from click.testing import CliRunner
5 | from pyq.pyq import main
6 |
7 |
8 | def pjoin(*path):
9 | return os.path.join(os.path.dirname(__file__), *path)
10 |
11 |
12 | class TestASTMatchEngine(unittest.TestCase):
13 | def setUp(self):
14 | self.runner = CliRunner()
15 |
16 | # chdir to testfiles/cmd
17 | self.currentdir = os.getcwd()
18 | os.chdir(pjoin('testfiles', 'cmd'))
19 |
20 | def tearDown(self):
21 | # restore cwd
22 | os.chdir(self.currentdir)
23 |
24 | def invoke(self, *args):
25 | return self.runner.invoke(*args, catch_exceptions=False)
26 |
27 | def test_noargs(self):
28 | result = self.invoke(main, [])
29 |
30 | self.assertNotEqual(result.exit_code, 0)
31 | self.assertIn('Missing argument "selector"', result.output_bytes)
32 |
33 | def test_nodir(self):
34 | result = self.invoke(main, ['def'])
35 | output = result.output_bytes.splitlines()
36 |
37 | self.assertEqual(result.exit_code, 0)
38 | self.assertEqual(len(output), 4)
39 | self.assertEqual(output[0], 'cmd.py:7 def foo(self):')
40 | self.assertEqual(output[1], 'cmd.py:11 def baz(arg1, arg2):')
41 | self.assertEqual(output[2], 'file2.py:1 def hello():')
42 | self.assertEqual(output[3], '.test_hidden_dir/foo.py:1 def bar():')
43 |
44 | def test_notpyfile(self):
45 | result = self.invoke(main, ['def', 'notpyfile.txt'])
46 |
47 | self.assertEqual(result.exit_code, 0)
48 |
49 | def test_file(self):
50 | result = self.invoke(main, ['> def', 'cmd.py'])
51 | output = result.output_bytes.splitlines()
52 |
53 | self.assertEqual(result.exit_code, 0)
54 | self.assertEqual(output[0], 'cmd.py:11 def baz(arg1, arg2):')
55 |
56 | def test_wildcard(self):
57 | result = self.invoke(main, ['def', 'cmd.py', 'file2.py',
58 | 'notpyfile.txt', 'nofile.unknown'])
59 | output = result.output_bytes.splitlines()
60 |
61 | self.assertEqual(result.exit_code, 0)
62 | self.assertEqual(len(output), 3)
63 | self.assertEqual(output[0], 'cmd.py:7 def foo(self):')
64 | self.assertEqual(output[1], 'cmd.py:11 def baz(arg1, arg2):')
65 | self.assertEqual(output[2], 'file2.py:1 def hello():')
66 |
67 | def test_print_filenames(self):
68 | result = self.invoke(main, ['-l', 'def'])
69 | output = result.output_bytes.splitlines()
70 |
71 | self.assertEqual(result.exit_code, 0)
72 | self.assertEqual(output[0], 'cmd.py')
73 | self.assertEqual(output[1], 'file2.py')
74 |
75 | def test_ignoredir(self):
76 | r = self.invoke(main, ['-l', 'class'])
77 | output = r.output_bytes.splitlines()
78 |
79 | self.assertEqual(r.exit_code, 0)
80 | self.assertTrue(any('ignoredir' in p for p in output))
81 |
82 | r = self.invoke(main, ['-l', 'class', '--ignore-dir', 'ignoredir'])
83 | output = r.output_bytes.splitlines()
84 |
85 | self.assertEqual(r.exit_code, 0)
86 | self.assertFalse(any('ignoredir' in p for p in output))
87 |
88 | def test_ignoredir_norecurse(self):
89 | r = self.invoke(main, ['-l', 'class', '--ignore-dir', 'ignoredir2'])
90 | output = r.output_bytes.splitlines()
91 |
92 | self.assertEqual(r.exit_code, 0)
93 | self.assertFalse(any('ignoredir2' in p for p in output))
94 |
95 | r = self.invoke(main, ['-l', 'class', '--ignore-dir', 'ignoredir2',
96 | '-n'])
97 | output = r.output_bytes.splitlines()
98 |
99 | self.assertEqual(r.exit_code, 0)
100 | self.assertTrue(any('ignoredir2' in p for p in output))
101 |
102 | def test_expand_matches(self):
103 | r1 = self.invoke(main, ['call', 'cmd.py'])
104 | r2 = self.invoke(main, ['-e', 'call', 'cmd.py'])
105 | r3 = self.invoke(main, ['-el', 'call', 'cmd.py'])
106 |
107 | output1 = r1.output_bytes.splitlines()
108 | output2 = r2.output_bytes.splitlines()
109 | output3 = r3.output_bytes.splitlines()
110 |
111 | self.assertEqual(r1.exit_code, 0)
112 | self.assertEqual(len(output1), 1)
113 | self.assertEqual(output1[0], 'cmd.py:15 foo() | bar()')
114 |
115 | self.assertEqual(r2.exit_code, 0)
116 | self.assertEqual(len(output2), 2)
117 | self.assertEqual(output2[0], 'cmd.py:15:0 foo() | bar()')
118 | self.assertEqual(output2[1], 'cmd.py:15:8 foo() | bar()')
119 |
120 | self.assertEqual(r3.exit_code, 0)
121 | self.assertEqual(len(output3), 1)
122 | self.assertEqual(output3[0], 'cmd.py')
123 |
124 |
125 | if __name__ == '__main__':
126 | unittest.main()
127 |
--------------------------------------------------------------------------------
/test_pyq.py:
--------------------------------------------------------------------------------
1 | from pyq.astmatch import ASTMatchEngine
2 |
3 | import unittest
4 | import os.path
5 | import ast
6 |
7 |
8 | class TestASTMatchEngine(unittest.TestCase):
9 | def setUp(self):
10 | self.m = ASTMatchEngine()
11 |
12 | def filepath(self, filename):
13 | return os.path.join(os.path.dirname(__file__), 'testfiles', filename)
14 |
15 | def test_classes(self):
16 | matches = list(self.m.match('class', self.filepath('classes.py')))
17 | self.assertEqual(len(matches), 4)
18 |
19 | # check instances
20 | self.assertIsInstance(matches[0][0], ast.ClassDef)
21 | self.assertIsInstance(matches[1][0], ast.ClassDef)
22 | self.assertIsInstance(matches[2][0], ast.ClassDef)
23 | self.assertIsInstance(matches[3][0], ast.ClassDef)
24 |
25 | # check lines
26 | self.assertEqual(matches[0][1], 1)
27 | self.assertEqual(matches[1][1], 9)
28 | self.assertEqual(matches[2][1], 13)
29 | self.assertEqual(matches[3][1], 14)
30 |
31 | def test_classes_with_specific_method(self):
32 | matches1 = list(self.m.match('class:not(:has(def))',
33 | self.filepath('classes.py')))
34 |
35 | matches2 = list(self.m.match('class:has(def[name=bar])',
36 | self.filepath('classes.py')))
37 |
38 | matches3 = list(self.m.match('class:has(> def)',
39 | self.filepath('classes.py')))
40 |
41 | self.assertEqual(len(matches1), 1)
42 | self.assertIsInstance(matches1[0][0], ast.ClassDef)
43 |
44 | self.assertEqual(len(matches2), 3)
45 | self.assertIsInstance(matches2[0][0], ast.ClassDef)
46 | self.assertIsInstance(matches2[1][0], ast.ClassDef)
47 | self.assertIsInstance(matches2[2][0], ast.ClassDef)
48 |
49 | self.assertEqual(len(matches3), 2)
50 | self.assertIsInstance(matches3[0][0], ast.ClassDef)
51 | self.assertIsInstance(matches3[1][0], ast.ClassDef)
52 | self.assertEqual(matches3[0][1], 1)
53 | self.assertEqual(matches3[1][1], 14)
54 |
55 | def test_methods(self):
56 | matches = list(self.m.match('class def', self.filepath('classes.py')))
57 | self.assertEqual(len(matches), 3)
58 |
59 | # check instances
60 | self.assertIsInstance(matches[0][0], ast.FunctionDef)
61 | self.assertIsInstance(matches[1][0], ast.FunctionDef)
62 | self.assertIsInstance(matches[2][0], ast.FunctionDef)
63 |
64 | # check lines
65 | self.assertEqual(matches[0][1], 2)
66 | self.assertEqual(matches[1][1], 5)
67 | self.assertEqual(matches[2][1], 15)
68 |
69 | def test_methods_by_name(self):
70 | matches1 = list(self.m.match('class def[name=baz]',
71 | self.filepath('classes.py')))
72 | matches2 = list(self.m.match('class def[name!=baz]',
73 | self.filepath('classes.py')))
74 |
75 | self.assertEqual(len(matches1), 1)
76 | self.assertEqual(matches1[0][1], 5)
77 |
78 | self.assertEqual(len(matches2), 2)
79 | self.assertEqual(matches2[0][1], 2)
80 | self.assertEqual(matches2[1][1], 15)
81 |
82 | def test_import(self):
83 | matches = list(self.m.match('import', self.filepath('imports.py')))
84 |
85 | self.assertEqual(len(matches), 6)
86 |
87 | # check instances
88 | self.assertIsInstance(matches[0][0], ast.ImportFrom)
89 | self.assertIsInstance(matches[1][0], ast.ImportFrom)
90 | self.assertIsInstance(matches[2][0], ast.ImportFrom)
91 | self.assertIsInstance(matches[3][0], ast.ImportFrom)
92 | self.assertIsInstance(matches[4][0], ast.Import)
93 | self.assertIsInstance(matches[5][0], ast.Import)
94 |
95 | def test_import_from(self):
96 | matches = list(self.m.match('import[from=foo]',
97 | self.filepath('imports.py')))
98 |
99 | self.assertEqual(len(matches), 2)
100 |
101 | # check instances
102 | self.assertIsInstance(matches[0][0], ast.ImportFrom)
103 | self.assertIsInstance(matches[1][0], ast.ImportFrom)
104 |
105 | # check lines
106 | self.assertEqual(matches[0][1], 1)
107 | self.assertEqual(matches[1][1], 2)
108 |
109 | def test_import_not_from(self):
110 | matches = list(self.m.match('import:not([from^=foo])',
111 | self.filepath('imports.py')))
112 |
113 | self.assertEqual(len(matches), 3)
114 |
115 | # check instances
116 | self.assertIsInstance(matches[0][0], ast.ImportFrom)
117 | self.assertIsInstance(matches[1][0], ast.Import)
118 | self.assertIsInstance(matches[2][0], ast.Import)
119 |
120 | def test_import_name(self):
121 | matches = list(self.m.match('import[name=example2]',
122 | self.filepath('imports.py')))
123 |
124 | self.assertEqual(len(matches), 1)
125 |
126 | matches = list(self.m.match('import[name=bar], import[name=bar2]',
127 | self.filepath('imports.py')))
128 |
129 | self.assertEqual(len(matches), 2)
130 |
131 | def test_import_multiple(self):
132 | matches1 = list(self.m.match('import[name=xyz]',
133 | self.filepath('imports.py')))
134 |
135 | matches2 = list(self.m.match('import[name=xyz][name=bar2]',
136 | self.filepath('imports.py')))
137 |
138 | matches3 = list(self.m.match('import[name=foo.baz]',
139 | self.filepath('imports.py')))
140 |
141 | matches4 = list(self.m.match('import[name^=foo]',
142 | self.filepath('imports.py')))
143 |
144 | matches5 = list(self.m.match('import[from^=foo]',
145 | self.filepath('imports.py')))
146 |
147 | self.assertEqual(len(matches1), 1)
148 | self.assertEqual(len(matches2), 1)
149 | self.assertEqual(len(matches3), 1)
150 | self.assertEqual(len(matches4), 1)
151 | self.assertEqual(len(matches5), 3)
152 |
153 | def test_import_special_attr(self):
154 | matches1 = list(self.m.match('import[full=foo.bar2]',
155 | self.filepath('imports.py')))
156 |
157 | matches2 = list(self.m.match('import[full=foo.xyz]',
158 | self.filepath('imports.py')))
159 |
160 | self.assertEqual(len(matches1), 1)
161 | self.assertEqual(len(matches2), 1)
162 |
163 | def test_match_id(self):
164 | matches = list(self.m.match('#foo,#bar', self.filepath('ids.py')))
165 |
166 | self.assertEqual(len(matches), 5)
167 |
168 | def test_assign(self):
169 | matches1 = list(self.m.match('assign', self.filepath('assign.py')))
170 | matches2 = list(self.m.match('assign#foo', self.filepath('assign.py')))
171 | matches3 = list(
172 | self.m.match('assign[name=bar]', self.filepath('assign.py')))
173 | matches4 = list(self.m.match('assign#b', self.filepath('assign.py')))
174 | matches5 = list(
175 | self.m.match('#abc,[name=abc]', self.filepath('assign.py')))
176 |
177 | self.assertEqual(len(matches1), 6)
178 |
179 | self.assertEqual(len(matches2), 1)
180 | self.assertEqual(matches2[0][1], 1)
181 |
182 | self.assertEqual(len(matches3), 1)
183 | self.assertEqual(matches3[0][1], 2)
184 |
185 | self.assertEqual(len(matches4), 1)
186 | self.assertEqual(matches4[0][1], 4)
187 |
188 | self.assertEqual(len(matches5), 1)
189 | self.assertEqual(matches5[0][1], 5)
190 |
191 | def test_calls(self):
192 | matches1 = list(
193 | self.m.match('[name=print]', self.filepath('calls.py')))
194 | matches2 = list(self.m.match('call#foo', self.filepath('calls.py')))
195 | matches3 = list(self.m.match('call', self.filepath('calls.py')))
196 | matches4 = list(self.m.match('[name=foo]', self.filepath('calls.py')))
197 |
198 | self.assertEqual(len(matches1), 1)
199 | self.assertEqual(len(matches2), 2)
200 | self.assertEqual(len(matches3), 5)
201 | self.assertEqual(len(matches4), 2)
202 |
203 | def test_call_arg_kwarg(self):
204 | matches1 = list(self.m.match('call[kwarg=a]',
205 | self.filepath('calls.py')))
206 | matches2 = list(self.m.match('call[kwarg=x]',
207 | self.filepath('calls.py')))
208 | matches3 = list(self.m.match('call[arg=bar]',
209 | self.filepath('calls.py')))
210 | matches4 = list(self.m.match('[arg=bang]', self.filepath('calls.py')))
211 |
212 | self.assertEqual(len(matches1), 1)
213 | self.assertEqual(len(matches2), 2)
214 | self.assertEqual(len(matches3), 2)
215 | self.assertEqual(len(matches4), 2)
216 |
217 | self.assertIsInstance(matches1[0][0], ast.Call)
218 | self.assertIsInstance(matches2[0][0], ast.Call)
219 | self.assertIsInstance(matches2[1][0], ast.Call)
220 | self.assertIsInstance(matches3[0][0], ast.Call)
221 | self.assertIsInstance(matches3[1][0], ast.Call)
222 | self.assertIsInstance(matches4[0][0], ast.Call)
223 |
224 | def test_attrs(self):
225 | matches1 = list(self.m.match('#bang', self.filepath('attrs.py')))
226 | matches2 = list(self.m.match('attr#z', self.filepath('attrs.py')))
227 | matches3 = list(self.m.match('attr', self.filepath('attrs.py')))
228 | matches4 = list(self.m.match('#y', self.filepath('attrs.py')))
229 |
230 | self.assertEqual(len(matches1), 1)
231 | self.assertEqual(len(matches2), 1)
232 | self.assertEqual(len(matches3), 4)
233 | self.assertEqual(len(matches4), 1)
234 |
235 | self.assertEqual(matches3[0][0].attr, 'bar')
236 | self.assertEqual(matches3[1][0].attr, 'bang')
237 | self.assertEqual(matches3[2][0].attr, 'y')
238 | self.assertEqual(matches3[3][0].attr, 'z')
239 |
240 | def test_pseudo_extends(self):
241 | matches1 = list(self.m.match(':extends(#object)',
242 | self.filepath('classes.py')))
243 |
244 | matches2 = list(self.m.match(':extends()',
245 | self.filepath('classes.py')))
246 |
247 | matches3 = list(self.m.match(':extends(#Unknown)',
248 | self.filepath('classes.py')))
249 |
250 | matches4 = list(self.m.match(':extends(#object, #X)',
251 | self.filepath('classes.py')))
252 |
253 | matches5 = list(self.m.match(':extends(#X):extends(#Y)',
254 | self.filepath('classes.py')))
255 |
256 | matches6 = list(self.m.match(':extends(#foo)',
257 | self.filepath('classes.py')))
258 |
259 | matches7 = list(self.m.match(':extends(attr#B)',
260 | self.filepath('classes.py')))
261 |
262 | matches8 = list(self.m.match(':extends(#A):extends(attr#B)',
263 | self.filepath('classes.py')))
264 |
265 | self.assertEqual(len(matches1), 3)
266 | self.assertEqual(len(matches2), 1)
267 | self.assertEqual(len(matches3), 0)
268 | self.assertEqual(len(matches4), 3)
269 | self.assertEqual(len(matches5), 1)
270 | self.assertEqual(len(matches6), 1)
271 | self.assertEqual(len(matches7), 1)
272 | self.assertEqual(len(matches8), 1)
273 |
274 |
275 | unittest.main(failfast=True)
276 |
--------------------------------------------------------------------------------
/test_sizzle.py:
--------------------------------------------------------------------------------
1 | from sizzle.selector import Selector
2 | from sizzle.match import MatchEngine
3 |
4 | import unittest
5 |
6 |
7 | class TestSObjs(unittest.TestCase):
8 | def test_type_selector(self):
9 | sobjs = Selector.parse('class')
10 |
11 | self.assertEqual(len(sobjs), 1)
12 | self.assertEqual(sobjs[0].name, 'class')
13 |
14 | def test_multiple_selectors(self):
15 | sobjs = Selector.parse('class, def')
16 |
17 | self.assertEqual(len(sobjs), 2)
18 | self.assertEqual(sobjs[0].name, 'class')
19 | self.assertEqual(sobjs[1].name, 'def')
20 |
21 | def test_composed_selector(self):
22 | sobjs = Selector.parse('class.foo.bar')
23 |
24 | self.assertEqual(len(sobjs), 1)
25 | self.assertIsInstance(sobjs[0], Selector)
26 | self.assertEqual(sobjs[0].typ, 'class')
27 | self.assertEqual(sobjs[0].classes, ['foo', 'bar'])
28 |
29 | def test_class_selector(self):
30 | sobjs = Selector.parse('.class > def')
31 |
32 | self.assertEqual(len(sobjs), 1)
33 | self.assertEqual(sobjs[0].name, '.class')
34 |
35 | def test_class_selector_child(self):
36 | sobjs = Selector.parse('.class > def')
37 |
38 | self.assertEqual(len(sobjs), 1)
39 | self.assertEqual(sobjs[0].name, '.class')
40 | self.assertIsNotNone(sobjs[0].next_selector)
41 |
42 | self.assertEqual(sobjs[0].next_selector.name, 'def')
43 | self.assertEqual(sobjs[0].next_selector.combinator,
44 | Selector.CHILD)
45 |
46 | def test_child_selector(self):
47 | sobjs = Selector.parse('class > def')
48 |
49 | self.assertEqual(len(sobjs), 1)
50 | self.assertIsInstance(sobjs[0], Selector)
51 | self.assertEqual(sobjs[0].name, 'class')
52 | self.assertIsNotNone(sobjs[0].next_selector)
53 |
54 | self.assertEqual(sobjs[0].next_selector.name, 'def')
55 | self.assertEqual(sobjs[0].next_selector.combinator, Selector.CHILD)
56 |
57 | def test_deep_selector(self):
58 | sobjs = Selector.parse('class > def foo > bar')
59 |
60 | self.assertEqual(len(sobjs), 1)
61 | self.assertEqual(sobjs[0].name, 'class')
62 | self.assertEqual(sobjs[0].next_selector.name, 'def')
63 | self.assertEqual(sobjs[0].next_selector.next_selector.name, 'foo')
64 | self.assertEqual(
65 | sobjs[0].next_selector.next_selector.next_selector.name, 'bar')
66 |
67 | self.assertEqual(sobjs[0].next_selector.name, 'def')
68 | self.assertEqual(sobjs[0].next_selector.combinator, Selector.CHILD)
69 |
70 | def test_nonchild_selector(self):
71 | sobjs = Selector.parse('class def')
72 |
73 | self.assertEqual(len(sobjs), 1)
74 | self.assertEqual(sobjs[0].name, 'class')
75 | self.assertIsNotNone(sobjs[0].next_selector)
76 |
77 | self.assertEqual(sobjs[0].next_selector.name, 'def')
78 |
79 | def test_pseudos(self):
80 | sobjs = Selector.parse(':not(1)')
81 |
82 | self.assertEqual(len(sobjs), 1)
83 | self.assertEqual(len(sobjs[0].pseudos), 1)
84 | self.assertEqual(sobjs[0].pseudos[0].name, 'not')
85 | self.assertEqual(sobjs[0].pseudos[0].value, '1')
86 |
87 | def test_pseudo_empty(self):
88 | sobjs = Selector.parse(':not()')
89 |
90 | self.assertEqual(len(sobjs), 1)
91 | self.assertEqual(len(sobjs[0].pseudos), 1)
92 | self.assertEqual(sobjs[0].pseudos[0].name, 'not')
93 | self.assertEqual(sobjs[0].pseudos[0].value, '')
94 |
95 | def test_nested_pseudos(self):
96 | sobjs = Selector.parse(':not(:not(1))')
97 |
98 | self.assertEqual(len(sobjs), 1)
99 | self.assertEqual(len(sobjs[0].pseudos), 1)
100 | self.assertEqual(sobjs[0].pseudos[0].name, 'not')
101 | self.assertEqual(sobjs[0].pseudos[0].value, ':not(1)')
102 |
103 | def test_compound_pseudos(self):
104 | sobjs = Selector.parse(':not(1):not(2)')
105 |
106 | self.assertEqual(len(sobjs), 1)
107 | self.assertEqual(len(sobjs[0].pseudos), 2)
108 | self.assertEqual(sobjs[0].pseudos[0].name, 'not')
109 | self.assertEqual(sobjs[0].pseudos[0].value, '1')
110 | self.assertEqual(sobjs[0].pseudos[1].name, 'not')
111 | self.assertEqual(sobjs[0].pseudos[1].value, '2')
112 |
113 | def test_attrs(self):
114 | sobjs = Selector.parse('[name=1]')
115 |
116 | self.assertEqual(len(sobjs), 1)
117 | self.assertEqual(len(sobjs[0].attrs), 1)
118 | self.assertEqual(sobjs[0].attrs[0].lft, 'name')
119 | self.assertEqual(sobjs[0].attrs[0].op, '=')
120 | self.assertEqual(sobjs[0].attrs[0].rgt, '1')
121 |
122 | def test_attrs_empty(self):
123 | sobjs = Selector.parse('[name!=]')
124 |
125 | self.assertEqual(len(sobjs), 1)
126 | self.assertEqual(len(sobjs[0].attrs), 1)
127 | self.assertEqual(sobjs[0].attrs[0].lft, 'name')
128 | self.assertEqual(sobjs[0].attrs[0].op, '!=')
129 | self.assertEqual(sobjs[0].attrs[0].rgt, '')
130 |
131 | def test_attrs_whitespace(self):
132 | sobjs = Selector.parse('[ name = 1 ]')
133 |
134 | self.assertEqual(len(sobjs), 1)
135 | self.assertEqual(len(sobjs[0].attrs), 1)
136 | self.assertEqual(sobjs[0].attrs[0].lft, 'name')
137 | self.assertEqual(sobjs[0].attrs[0].op, '=')
138 | self.assertEqual(sobjs[0].attrs[0].rgt, '1')
139 |
140 | def test_deep_attrs(self):
141 | sobjs = Selector.parse('[name=1] > [name=2]')
142 |
143 | self.assertEqual(len(sobjs), 1)
144 | self.assertEqual(len(sobjs[0].attrs), 1)
145 | self.assertEqual(sobjs[0].attrs[0].lft, 'name')
146 | self.assertEqual(sobjs[0].attrs[0].op, '=')
147 | self.assertEqual(sobjs[0].attrs[0].rgt, '1')
148 | self.assertEqual(sobjs[0].next_selector.attrs[0].lft, 'name')
149 | self.assertEqual(sobjs[0].next_selector.attrs[0].op, '=')
150 | self.assertEqual(sobjs[0].next_selector.attrs[0].rgt, '2')
151 |
152 | def test_attr_in_pseudo(self):
153 | sobjs = Selector.parse(':not([name=1])')
154 |
155 | self.assertEqual(len(sobjs), 1)
156 | self.assertEqual(len(sobjs[0].attrs), 0)
157 | self.assertEqual(len(sobjs[0].pseudos), 1)
158 | self.assertEqual(sobjs[0].pseudos[0].name, 'not')
159 | self.assertEqual(sobjs[0].pseudos[0].value, '[name=1]')
160 |
161 |
162 | class CustomMatchEngine(MatchEngine):
163 | def __init__(self):
164 | super(CustomMatchEngine, self).__init__()
165 | self.register_pseudo('extends', self.pseudo_extends)
166 |
167 | @staticmethod
168 | def pseudo_extends(matcher, node, value):
169 | if not value:
170 | return hasattr(node, 'extends') and not node.extends
171 | return value in getattr(node, 'extends', [])
172 |
173 | def match_type(self, typ, node):
174 | if typ == 'class':
175 | cls = self.CLS
176 | elif typ == 'def':
177 | cls = self.DEF
178 |
179 | return isinstance(node, cls)
180 |
181 | def match_id(self, id_, node):
182 | return node.name == id_
183 |
184 | def iter_data(self, data):
185 | for node in data:
186 | yield node, getattr(node, 'body', None)
187 |
188 |
189 | class TestCustomMatcher(unittest.TestCase):
190 | from collections import namedtuple
191 |
192 | CLS = namedtuple('ClassDef', ('name', 'extends', 'body'))
193 | DEF = namedtuple('FunctionDef', ('name',))
194 |
195 | def setUp(self):
196 | self.data = [
197 | self.CLS('Test', ['object'], [
198 | self.DEF('foo'),
199 | self.DEF('bar'),
200 | self.CLS('Test2', [], [
201 | self.DEF('baz'),
202 | ]),
203 | self.CLS('Test3', [], [
204 | self.CLS('Test5', [], [
205 | self.DEF('bang'),
206 | ]),
207 | ]),
208 | ]),
209 | self.DEF('baz'),
210 | self.CLS('Test4', [], []),
211 | ]
212 |
213 | self.matcher = CustomMatchEngine()
214 | self.matcher.CLS = self.CLS
215 | self.matcher.DEF = self.DEF
216 |
217 | self.match = lambda s: list(self.matcher.match(s, self.data))
218 |
219 | def test_typ(self):
220 | self.assertEqual(len(self.match('class')), 5)
221 | self.assertEqual(len(self.match('class, def')), 10)
222 |
223 | def test_child(self):
224 | self.assertEqual(len(self.match('class > def')), 4)
225 |
226 | def test_ids(self):
227 | self.assertEqual(len(self.match('def#bla')), 0)
228 | self.assertEqual(len(self.match('def#foo')), 1)
229 |
230 | def test_pseudos(self):
231 | self.assertEqual(len(self.match(':extends(object)')), 1)
232 |
233 | def test_pseudos_noargs(self):
234 | self.assertEqual(len(self.match(':extends()')), 4)
235 |
236 | def test_nested_pseudos(self):
237 | self.assertEqual(
238 | len(self.match(':not(:extends(object))')), 9)
239 | self.assertEqual(
240 | len(self.match(':extends(object):not(def)')), 1)
241 | self.assertEqual(
242 | len(self.match(':not(def)')), 5)
243 |
244 | def test_pseudo_has(self):
245 | self.assertEqual(len(self.match(':has(def)')), 4)
246 | self.assertEqual(len(self.match(':has(> def)')), 3)
247 |
248 |
249 | unittest.main(failfast=True)
250 |
--------------------------------------------------------------------------------
/testfiles/assign.py:
--------------------------------------------------------------------------------
1 | foo = 1
2 | bar, zzz = foo
3 | [x, y] = 2
4 | (a, (b,)) = 3
5 | abc['def'] = None
6 | bla()['x'] = 3
7 |
--------------------------------------------------------------------------------
/testfiles/attrs.py:
--------------------------------------------------------------------------------
1 | foo.bar.bang
2 |
3 | x = x.y.z
4 |
--------------------------------------------------------------------------------
/testfiles/calls.py:
--------------------------------------------------------------------------------
1 | print('Hello world')
2 |
3 | foo(bar, a=None, x=1)
4 | foo()(bar, bang, x=2)
5 |
6 | x = Foo(bang)
7 |
--------------------------------------------------------------------------------
/testfiles/classes.py:
--------------------------------------------------------------------------------
1 | class Foo(object):
2 | def bar(self):
3 | pass
4 |
5 | def baz(self):
6 | pass
7 |
8 |
9 | class Bar(object, X, Y, foo(bar), A.B):
10 | test = None
11 |
12 |
13 | class Bang:
14 | class Baz(object):
15 | def bar(self):
16 | x = [0]
17 | x[0] = 1
18 |
19 |
20 | import dummy_import
21 |
--------------------------------------------------------------------------------
/testfiles/cmd/.test_hidden_dir/foo.py:
--------------------------------------------------------------------------------
1 | def bar():
2 | pass
3 |
--------------------------------------------------------------------------------
/testfiles/cmd/cmd.py:
--------------------------------------------------------------------------------
1 | class Foo(object):
2 | pass
3 |
4 |
5 | @decorator
6 | class Bar(object):
7 | def foo(self):
8 | pass
9 |
10 |
11 | def baz(arg1, arg2):
12 | pass
13 |
14 |
15 | foo() | bar()
16 |
--------------------------------------------------------------------------------
/testfiles/cmd/file2.py:
--------------------------------------------------------------------------------
1 | def hello():
2 | print('Hello world')
3 |
--------------------------------------------------------------------------------
/testfiles/cmd/ignoredir/ignoredir2/test.py:
--------------------------------------------------------------------------------
1 | class Foo(object):
2 | pass
3 |
--------------------------------------------------------------------------------
/testfiles/cmd/ignoredir/test.py:
--------------------------------------------------------------------------------
1 | class Foo(object):
2 | pass
3 |
--------------------------------------------------------------------------------
/testfiles/cmd/notpyfile.txt:
--------------------------------------------------------------------------------
1 | ():#!
2 |
--------------------------------------------------------------------------------
/testfiles/ids.py:
--------------------------------------------------------------------------------
1 | class foo:
2 | pass
3 |
4 | foo = 1
5 | bar = foo
6 |
7 | def bar(baz, bang=1, foo=2):
8 | pass
9 |
--------------------------------------------------------------------------------
/testfiles/imports.py:
--------------------------------------------------------------------------------
1 | from foo import bar # noqa
2 | from foo import bar as bar2, xyz # noqa
3 | from foo.baz import bang # noqa
4 | from . import x
5 |
6 | import example as example2 # noqa
7 | import foo.baz # noqa
8 |
--------------------------------------------------------------------------------