Conflicts: static/js/index.js templates/index.htmltags/v1.0^2
@@ -18,12 +18,14 @@ class Codelet(object): | |||||
code was last modified. | code was last modified. | ||||
:ivar rank: (float) A quanitification of the source code's quality, as | :ivar rank: (float) A quanitification of the source code's quality, as | ||||
per available ratings (stars, forks, upvotes, etc.). | per available ratings (stars, forks, upvotes, etc.). | ||||
:ivar symbols: (dict) Dictionary containing dictionaries of functions, classes, | |||||
variable definitions, etc. | |||||
:ivar symbols: (dict) Dictionary containing dictionaries of functions, | |||||
classes, variable definitions, etc. | |||||
:ivar origin: (tuple) 3-tuple of (site_name, site_url, image_blob), as | |||||
added by the database. | |||||
""" | """ | ||||
def __init__(self, name, code, filename, language, authors, code_url, | def __init__(self, name, code, filename, language, authors, code_url, | ||||
date_created, date_modified, rank): | |||||
date_created, date_modified, rank, symbols=None, origin=None): | |||||
""" | """ | ||||
Create a Codelet instance. | Create a Codelet instance. | ||||
@@ -36,6 +38,8 @@ class Codelet(object): | |||||
:param date_created: see :attr:`self.date_created` | :param date_created: see :attr:`self.date_created` | ||||
:param date_modified: see :attr:`self.date_modified` | :param date_modified: see :attr:`self.date_modified` | ||||
:param rank: see :attr:`self.rank` | :param rank: see :attr:`self.rank` | ||||
:param symbols: see :attr:`self.symbols` | |||||
:param origin: see :attr:`self.origin` | |||||
:type name: see :attr:`self.name` | :type name: see :attr:`self.name` | ||||
:type code: see :attr:`self.code` | :type code: see :attr:`self.code` | ||||
@@ -46,6 +50,8 @@ class Codelet(object): | |||||
:type date_created: see :attr:`self.date_created` | :type date_created: see :attr:`self.date_created` | ||||
:type date_modified: see :attr:`self.date_modified` | :type date_modified: see :attr:`self.date_modified` | ||||
:type rank: see :attr:`self.rank` | :type rank: see :attr:`self.rank` | ||||
:type symbols: see :attr:`self.symbols` | |||||
:type origin: see :attr:`self.origin` | |||||
""" | """ | ||||
self.name = name | self.name = name | ||||
@@ -57,3 +63,5 @@ class Codelet(object): | |||||
self.date_created = date_created | self.date_created = date_created | ||||
self.date_modified = date_modified | self.date_modified = date_modified | ||||
self.rank = rank | self.rank = rank | ||||
self.symbols = symbols or {} | |||||
self.origin = origin or (None, None, None) |
@@ -9,6 +9,9 @@ import mmh3 | |||||
import oursql | import oursql | ||||
from .migration import VERSION, MIGRATIONS | from .migration import VERSION, MIGRATIONS | ||||
from ..codelet import Codelet | |||||
from ..query.nodes import (String, Regex, Text, Language, Author, Date, Symbol, | |||||
BinaryOp, UnaryOp) | |||||
__all__ = ["Database"] | __all__ = ["Database"] | ||||
@@ -51,9 +54,71 @@ class Database(object): | |||||
"Run `python -m bitshift.database.migration`." | "Run `python -m bitshift.database.migration`." | ||||
raise RuntimeError(err) | raise RuntimeError(err) | ||||
def _search_with_query(self, cursor, tree, page): | |||||
"""Execute an SQL query based on a query tree, and return results. | |||||
The returned data is a 2-tuple of (list of codelet IDs, estimated | |||||
number of total results). | |||||
""" | |||||
query, args = tree.build_query(page) | |||||
cursor.execute(query, args) | |||||
ids = [id for id, _ in cursor.fetchall()] | |||||
num_results = 0 # TODO: NotImplemented | |||||
return ids, num_results | |||||
def _get_authors_for_codelet(self, cursor, codelet_id): | |||||
"""Return a list of authors for a given codelet.""" | |||||
query = """SELECT author_name, author_url | |||||
FROM authors | |||||
WHERE author_codelet = ?""" | |||||
cursor.execute(query, (codelet_id,)) | |||||
return cursor.fetchall() | |||||
def _get_symbols_for_code(self, cursor, code_id): | |||||
"""Return a list of symbols for a given codelet.""" | |||||
query = """SELECT symbol_type, symbol_name, sloc_type, sloc_row, | |||||
sloc_col, sloc_end_row, sloc_end_col | |||||
FROM symbols | |||||
INNER JOIN symbol_locations ON sloc_symbol = symbol_id | |||||
WHERE symbol_code = ?""" | |||||
symbols = {type_: {} for type_ in Symbol.TYPES_INV} | |||||
cursor.execute(query, (code_id,)) | |||||
for type_, name, loc_type, row, col, erow, ecol in cursor.fetchall(): | |||||
sdict = symbols[Symbol.TYPES_INV[type_]] | |||||
if name not in sdict: | |||||
sdict[name] = ((), ()) | |||||
sdict[name][loc_type].append((row, col, erow, ecol)) | |||||
for type_, sdict in symbols.items(): | |||||
symbols[type_] = [(n, d, u) for n, (d, u) in sdict.iteritems()] | |||||
return symbols | |||||
def _get_codelets_from_ids(self, cursor, ids): | def _get_codelets_from_ids(self, cursor, ids): | ||||
"""Return a list of Codelet objects given a list of codelet IDs.""" | """Return a list of Codelet objects given a list of codelet IDs.""" | ||||
raise NotImplementedError() ## TODO | |||||
query = """SELECT * | |||||
FROM codelets | |||||
INNER JOIN code ON codelet_code_id = code_id | |||||
INNER JOIN origins ON codelet_origin = origin_id | |||||
WHERE codelet_id = ?""" | |||||
with self._conn.cursor(oursql.DictCursor) as dict_cursor: | |||||
dict_cursor.executemany(query, [(id,) for id in ids]) | |||||
for row in dict_cursor.fetchone(): | |||||
codelet_id = row["codelet_id"] | |||||
if row["origin_url_base"]: | |||||
url = row["codelet_url"] | |||||
else: | |||||
url = row["origin_url_base"] + row["codelet_url"] | |||||
origin = (row["origin_name"], row["origin_url"], | |||||
row["origin_image"]) | |||||
authors = self._get_authors_for_codelet(cursor, codelet_id) | |||||
symbols = self._get_symbols_for_code(cursor, row["code_id"]) | |||||
yield Codelet( | |||||
row["codelet_name"], row["code_code"], None, | |||||
row["code_lang"], authors, url, | |||||
row["codelet_date_created"], row["codelet_date_modified"], | |||||
row["codelet_rank"], symbols, origin) | |||||
def _decompose_url(self, cursor, url): | def _decompose_url(self, cursor, url): | ||||
"""Break up a URL into an origin (with a URL base) and a suffix.""" | """Break up a URL into an origin (with a URL base) and a suffix.""" | ||||
@@ -68,13 +133,12 @@ class Database(object): | |||||
def _insert_symbols(self, cursor, code_id, sym_type, symbols): | def _insert_symbols(self, cursor, code_id, sym_type, symbols): | ||||
"""Insert a list of symbols of a given type into the database.""" | """Insert a list of symbols of a given type into the database.""" | ||||
sym_types = ["functions", "classes", "variables"] | |||||
query1 = "INSERT INTO symbols VALUES (DEFAULT, ?, ?, ?)" | query1 = "INSERT INTO symbols VALUES (DEFAULT, ?, ?, ?)" | ||||
query2 = """INSERT INTO symbol_locations VALUES | query2 = """INSERT INTO symbol_locations VALUES | ||||
(DEFAULT, ?, ?, ?, ?, ?, ?)""" | (DEFAULT, ?, ?, ?, ?, ?, ?)""" | ||||
for (name, decls, uses) in symbols: | for (name, decls, uses) in symbols: | ||||
cursor.execute(query1, (code_id, sym_types.index(sym_type), name)) | |||||
cursor.execute(query1, (code_id, Symbol.TYPES_INV[sym_type], name)) | |||||
sym_id = cursor.lastrowid | sym_id = cursor.lastrowid | ||||
params = ([tuple([sym_id, 0] + list(loc)) for loc in decls] + | params = ([tuple([sym_id, 0] + list(loc)) for loc in decls] + | ||||
[tuple([sym_id, 1] + list(loc)) for loc in uses]) | [tuple([sym_id, 1] + list(loc)) for loc in uses]) | ||||
@@ -112,16 +176,14 @@ class Database(object): | |||||
num_results = results[0][1] * (10 ** results[0][2]) | num_results = results[0][1] * (10 ** results[0][2]) | ||||
ids = [res[0] for res in results] | ids = [res[0] for res in results] | ||||
else: # Cache miss | else: # Cache miss | ||||
## TODO: build and execute search query | |||||
results = cursor.fetchall() | |||||
ids = NotImplemented ## TODO: extract ids from results | |||||
num_results = NotImplemented ## TODO: num if results else 0 | |||||
ids, num_results = self._search_with_query(cursor, query, page) | |||||
num_exp = max(len(str(num_results)) - 3, 0) | num_exp = max(len(str(num_results)) - 3, 0) | ||||
num_results = int(round(num_results, -num_exp)) | num_results = int(round(num_results, -num_exp)) | ||||
num_mnt = num_results / (10 ** num_exp) | num_mnt = num_results / (10 ** num_exp) | ||||
cursor.execute(query2, (cache_id, num_mnt, num_exp)) | cursor.execute(query2, (cache_id, num_mnt, num_exp)) | ||||
cursor.executemany(query3, [(cache_id, c_id) for c_id in ids]) | cursor.executemany(query3, [(cache_id, c_id) for c_id in ids]) | ||||
return (num_results, self._get_codelets_from_ids(cursor, ids)) | |||||
codelet_gen = self._get_codelets_from_ids(cursor, ids) | |||||
return (num_results, list(codelet_gen)) | |||||
def insert(self, codelet): | def insert(self, codelet): | ||||
""" | """ | ||||
@@ -21,7 +21,7 @@ def _lang(codelet): | |||||
if codelet.filename is not None: | if codelet.filename is not None: | ||||
try: | try: | ||||
return pgl.guess_lexer_for_filename(codelet.filename, '').name | |||||
return pgl.guess_lexer_for_filename(codelet.filename, codelet.code).name | |||||
except: | except: | ||||
raise UnsupportedFileError('Could not find a lexer for the codelet\'s filename') | raise UnsupportedFileError('Could not find a lexer for the codelet\'s filename') | ||||
@@ -1,11 +1,283 @@ | |||||
from .association import Association | |||||
from .node import Node | |||||
""" | |||||
This subpackage contains code to parse search queries received from the | |||||
frontend into trees that can be used by the database backend. | |||||
""" | |||||
from __future__ import unicode_literals | |||||
from re import IGNORECASE, search | |||||
from sys import maxsize | |||||
from dateutil.parser import parse as parse_date | |||||
from .nodes import (String, Regex, Text, Language, Author, Date, Symbol, | |||||
BinaryOp, UnaryOp) | |||||
from .tree import Tree | from .tree import Tree | ||||
from ..languages import LANGS | |||||
__all__ = ["parse_query"] | |||||
__all__ = ["QueryParseException", "parse_query"] | |||||
def parse_query(query): | |||||
# gets a string, returns a Tree | |||||
# TODO: note: resultant Trees should be normalized so that "foo OR bar" | |||||
# and "bar OR foo" result in equivalent trees | |||||
class QueryParseException(Exception): | |||||
"""Raised by parse_query() when a query is invalid.""" | |||||
pass | pass | ||||
class _QueryParser(object): | |||||
"""Wrapper class with methods to parse queries. Used as a singleton.""" | |||||
def __init__(self): | |||||
self._prefixes = { | |||||
self._parse_language: ["l", "lang", "language"], | |||||
self._parse_author: ["a", "author"], | |||||
self._parse_modified: ["m", "mod", "modified", "modify"], | |||||
self._parse_created: ["cr", "create", "created"], | |||||
self._parse_symbol: ["s", "sym", "symb", "symbol"], | |||||
self._parse_function: ["f", "fn", "fun", "func", "function"], | |||||
self._parse_class: ["cl", "class", "clss"], | |||||
self._parse_variable: ["v", "var", "variable"] | |||||
} | |||||
def _parse_literal(self, literal): | |||||
"""Parse part of a search query into a string or regular expression.""" | |||||
if literal.startswith(("r:", "re:", "regex:", "regexp:")): | |||||
return Regex(literal.split(":", 1)[1]) | |||||
return String(literal) | |||||
def _parse_language(self, term): | |||||
"""Parse part of a query into a language node and return it.""" | |||||
term = self._parse_literal(term) | |||||
if isinstance(term, Regex): | |||||
langs = [i for i, lang in enumerate(LANGS) | |||||
if search(term.regex, lang, IGNORECASE)] | |||||
if not langs: | |||||
err = 'No languages found for regex: "%s"' % term.regex | |||||
raise QueryParseException(err) | |||||
node = Language(langs.pop()) | |||||
while langs: | |||||
node = BinaryOp(Language(langs.pop()), BinaryOp.OR, node) | |||||
return node | |||||
needle = term.string.lower() | |||||
for i, lang in enumerate(LANGS): | |||||
if lang.lower() == needle: | |||||
return Language(i) | |||||
for i, lang in enumerate(LANGS): | |||||
if lang.lower().startswith(needle): | |||||
return Language(i) | |||||
err = 'No languages found for string: "%s"' % term.string | |||||
raise QueryParseException(err) | |||||
def _parse_author(self, term): | |||||
"""Parse part of a query into an author node and return it.""" | |||||
return Author(self._parse_literal(term)) | |||||
def _parse_date(self, term, type_): | |||||
"""Parse part of a query into a date node and return it.""" | |||||
if ":" not in term: | |||||
err = "A date relationship is required " \ | |||||
'("before:<date>" or "after:<date>"): "%s"' | |||||
raise QueryParseException(err % term) | |||||
relstr, dtstr = term.split(":", 1) | |||||
if relstr.lower() in ("before", "b"): | |||||
relation = Date.BEFORE | |||||
elif relstr.lower() in ("after", "a"): | |||||
relation = Date.AFTER | |||||
else: | |||||
err = 'Bad date relationship (should be "before" or "after"): "%s"' | |||||
raise QueryParseException(err % relstr) | |||||
try: | |||||
dt = parse_date(dtstr) | |||||
except (TypeError, ValueError): | |||||
raise QueryParseException('Bad date/time string: "%s"' % dtstr) | |||||
return Date(type_, relation, dt) | |||||
def _parse_modified(self, term): | |||||
"""Parse part of a query into a date modified node and return it.""" | |||||
return self._parse_date(term, Date.MODIFY) | |||||
def _parse_created(self, term): | |||||
"""Parse part of a query into a date created node and return it.""" | |||||
return self._parse_date(term, Date.CREATE) | |||||
def _parse_symbol(self, term): | |||||
"""Parse part of a query into a symbol node and return it.""" | |||||
return Symbol(Symbol.ALL, self._parse_literal(term)) | |||||
def _parse_function(self, term): | |||||
"""Parse part of a query into a function node and return it.""" | |||||
return Symbol(Symbol.FUNCTION, self._parse_literal(term)) | |||||
def _parse_class(self, term): | |||||
"""Parse part of a query into a class node and return it.""" | |||||
return Symbol(Symbol.CLASS, self._parse_literal(term)) | |||||
def _parse_variable(self, term): | |||||
"""Parse part of a query into a variable node and return it.""" | |||||
return Symbol(Symbol.VARIABLE, self._parse_literal(term)) | |||||
def _parse_term(self, term): | |||||
"""Parse a query term into a tree node and return it.""" | |||||
try: | |||||
term = term.decode("unicode_escape") | |||||
except UnicodeDecodeError: | |||||
raise QueryParseException('Invalid query term: "%s"' % term) | |||||
if ":" in term and not term[0] == ":": | |||||
prefix, arg = term.split(":", 1) | |||||
invert = prefix.lower() == "not" | |||||
if invert: | |||||
prefix, arg = arg.split(":", 1) | |||||
if not arg: | |||||
raise QueryParseException('Incomplete query term: "%s"' % term) | |||||
for meth, prefixes in self._prefixes.iteritems(): | |||||
if prefix.lower() in prefixes: | |||||
if invert: | |||||
return UnaryOp(UnaryOp.NOT, meth(arg)) | |||||
return meth(arg) | |||||
return Text(self._parse_literal(term)) | |||||
def _scan_query(self, query, markers): | |||||
"""Scan a query (sub)string for the first occurance of some markers. | |||||
Returns a 2-tuple of (first_marker_found, marker_index). | |||||
""" | |||||
def is_escaped(query, index): | |||||
"""Return whether a query marker is backslash-escaped.""" | |||||
return (index > 0 and query[index - 1] == "\\" and | |||||
(index < 2 or query[index - 2] != "\\")) | |||||
best_marker, best_index = None, maxsize | |||||
for marker in markers: | |||||
index = query.find(marker) | |||||
if is_escaped(query, index): | |||||
_, new_index = self._scan_query(query[index + 1:], marker) | |||||
index += new_index + 1 | |||||
if index >= 0 and index < best_index: | |||||
best_marker, best_index = marker, index | |||||
return best_marker, best_index | |||||
def _split_query(self, query, parens=False): | |||||
"""Split a query string into a nested list of query terms. | |||||
Returns a list of terms and/or nested sublists of terms. Each term and | |||||
sublist is guarenteed to be non-empty. | |||||
""" | |||||
query = query.lstrip() | |||||
if not query: | |||||
return [] | |||||
marker, index = self._scan_query(query, " \"'()") | |||||
if not marker: | |||||
return [query] | |||||
nest = [query[:index]] if index > 0 else [] | |||||
after = query[index + 1:] | |||||
if marker == " ": | |||||
nest += self._split_query(after, parens) | |||||
elif marker in ('"', "'"): | |||||
close_marker, close_index = self._scan_query(after, marker) | |||||
if close_marker: | |||||
if close_index > 0: | |||||
nest.append(after[:close_index]) | |||||
after = after[close_index + 1:] | |||||
nest += self._split_query(after, parens) | |||||
elif after: | |||||
nest.append(after) | |||||
elif marker == "(": | |||||
inner, after = self._split_query(after, True), [] | |||||
if inner and isinstance(inner[-1], tuple): | |||||
after = self._split_query(inner.pop()[0], parens) | |||||
if inner: | |||||
nest.append(inner) | |||||
if after: | |||||
nest += after | |||||
elif marker == ")": | |||||
if parens: | |||||
nest.append((after,)) | |||||
else: | |||||
nest += self._split_query(after) | |||||
return nest | |||||
def _parse_boolean_operators(self, nest): | |||||
"""Parse boolean operators in a nested query list.""" | |||||
op_lookup = { | |||||
"and": BinaryOp.AND, | |||||
"or": BinaryOp.OR, | |||||
"not": UnaryOp.NOT | |||||
} | |||||
for i, term in enumerate(nest): | |||||
if isinstance(term, list): | |||||
self._parse_boolean_operators(term) | |||||
else: | |||||
nest[i] = op_lookup.get(term.lower(), term) | |||||
def _parse_nest(self, nest): | |||||
"""Recursively parse a nested list of search query terms.""" | |||||
def parse_binary_op(op): | |||||
"""Parse a binary operator in a nested query list.""" | |||||
index = nest.index(op) | |||||
if index == 0 or index == len(nest) - 1: | |||||
err = "Invalid query: '%s' given without argument." | |||||
raise QueryParseException(err % BinaryOp.OPS[op]) | |||||
left = self._parse_nest(nest[:index]) | |||||
right = self._parse_nest(nest[index + 1:]) | |||||
return BinaryOp(left, op, right) | |||||
if not nest: | |||||
err = "Error while parsing query: empty nest detected." | |||||
raise QueryParseException(err) | |||||
elif BinaryOp.OR in nest: | |||||
return parse_binary_op(BinaryOp.OR) | |||||
elif BinaryOp.AND in nest: | |||||
return parse_binary_op(BinaryOp.AND) | |||||
elif UnaryOp.NOT in nest: | |||||
index = nest.index(UnaryOp.NOT) | |||||
if index == len(nest) - 1: | |||||
err = "Invalid query: '%s' given without argument." | |||||
raise QueryParseException(err % UnaryOp.OPS[UnaryOp.NOT]) | |||||
right = UnaryOp(UnaryOp.NOT, self._parse_nest(nest[index + 1:])) | |||||
if index > 0: | |||||
left = self._parse_nest(nest[:index]) | |||||
return BinaryOp(left, BinaryOp.AND, right) | |||||
return right | |||||
elif len(nest) > 1: | |||||
left, right = self._parse_term(nest[0]), self._parse_nest(nest[1:]) | |||||
return BinaryOp(left, BinaryOp.AND, right) | |||||
elif isinstance(nest[0], list): | |||||
return self._parse_nest(nest[0]) | |||||
else: | |||||
return self._parse_term(nest[0]) | |||||
def _balance_tree(self, node): | |||||
"""Auto-balance a tree using a string sorting function.""" | |||||
if isinstance(node, BinaryOp): | |||||
self._balance_tree(node.left) | |||||
self._balance_tree(node.right) | |||||
if node.right.sortkey() < node.left.sortkey(): | |||||
node.left, node.right = node.right, node.left | |||||
elif isinstance(node, UnaryOp): | |||||
self._balance_tree(node.node) | |||||
def parse(self, query): | |||||
""" | |||||
Parse a search query. | |||||
The result is normalized with a sorting function so that | |||||
``"foo OR bar"`` and ``"bar OR foo"`` result in the same tree. This is | |||||
important for caching purposes. | |||||
:param query: The query be converted. | |||||
:type query: str | |||||
:return: A tree storing the data in the query. | |||||
:rtype: :py:class:`~.query.tree.Tree` | |||||
:raises: :py:class:`.QueryParseException` | |||||
""" | |||||
nest = self._split_query(query.rstrip()) | |||||
if not nest: | |||||
raise QueryParseException('Empty query: "%s"' % query) | |||||
self._parse_boolean_operators(nest) | |||||
root = self._parse_nest(nest) | |||||
self._balance_tree(root) | |||||
return Tree(root) | |||||
parse_query = _QueryParser().parse |
@@ -0,0 +1,276 @@ | |||||
from ..languages import LANGS | |||||
__all__ = ["String", "Regex", "Text", "Language", "Author", "Date", "Symbol", | |||||
"BinaryOp", "UnaryOp"] | |||||
class _Node(object): | |||||
"""Represents a single node in a query tree. | |||||
Generally speaking, a node is a constraint applied to the database. Thus, | |||||
a :py:class:`~.Language` node represents a constraint where only codelets | |||||
of a specific language are selected. | |||||
""" | |||||
def sortkey(self): | |||||
"""Return a string sort key for the node.""" | |||||
return "" | |||||
def parameterize(self, tables): | |||||
"""Parameterize the node. | |||||
Returns a 4-tuple of (conditional string, parameter list, rank list, | |||||
should-we-rank boolean). If the rank list is empty, then it is assumed | |||||
to contain the conditional string. | |||||
""" | |||||
return "", [], [], False | |||||
class _Literal(object): | |||||
"""Represents a literal component of a search query, present at the leaves. | |||||
A literal might be a string or a regular expression. | |||||
""" | |||||
pass | |||||
class String(_Literal): | |||||
"""Represents a string literal.""" | |||||
def __init__(self, string): | |||||
""" | |||||
:type string: unicode | |||||
""" | |||||
self.string = string | |||||
def __repr__(self): | |||||
return "String({0!r})".format(self.string) | |||||
def sortkey(self): | |||||
return self.string | |||||
class Regex(_Literal): | |||||
"""Represents a regular expression literal.""" | |||||
def __init__(self, regex): | |||||
""" | |||||
:type string: unicode | |||||
""" | |||||
self.regex = regex | |||||
def __repr__(self): | |||||
return "Regex({0!r})".format(self.regex) | |||||
def sortkey(self): | |||||
return self.regex | |||||
class Text(_Node): | |||||
"""Represents a text node. | |||||
Searches in codelet names (full-text search), symbols (equality), and | |||||
source code (full-text search). | |||||
""" | |||||
def __init__(self, text): | |||||
""" | |||||
:type text: :py:class:`._Literal` | |||||
""" | |||||
self.text = text | |||||
def __repr__(self): | |||||
return "Text({0})".format(self.text) | |||||
def sortkey(self): | |||||
return self.text.sortkey() | |||||
def parameterize(self, tables): | |||||
tables |= {"code", "symbols"} | |||||
if isinstance(self.text, Regex): | |||||
ranks = ["(codelet_name REGEXP ?)", "(symbol_name REGEXP ?)", | |||||
"(code_code REGEXP ?)"] | |||||
text = self.text.regex | |||||
else: | |||||
ranks = ["(MATCH(codelet_name) AGAINST (? IN BOOLEAN MODE))", | |||||
"(MATCH(code_code) AGAINST (? IN BOOLEAN MODE))", | |||||
"(symbol_name = ?)"] | |||||
text = self.text.string | |||||
cond = "(" + " OR ".join(ranks) + ")" | |||||
return cond, [text] * 3, ranks, True | |||||
class Language(_Node): | |||||
"""Represents a language node. | |||||
Searches in the code_lang field. | |||||
""" | |||||
def __init__(self, lang): | |||||
""" | |||||
:type lang: int | |||||
""" | |||||
self.lang = lang | |||||
def __repr__(self): | |||||
return "Language({0})".format(LANGS[self.lang]) | |||||
def sortkey(self): | |||||
return LANGS[self.lang] | |||||
def parameterize(self, tables): | |||||
tables |= {"code"} | |||||
return "(code_lang = ?)", [self.lang], [], False | |||||
class Author(_Node): | |||||
"""Represents a author node. | |||||
Searches in the author_name field (full-text search). | |||||
""" | |||||
def __init__(self, name): | |||||
""" | |||||
:type name: :py:class:`_Literal` | |||||
""" | |||||
self.name = name | |||||
def __repr__(self): | |||||
return "Author({0})".format(self.name) | |||||
def sortkey(self): | |||||
return self.name.sortkey() | |||||
def parameterize(self, tables): | |||||
tables |= {"authors"} | |||||
if isinstance(self.name, Regex): | |||||
return "(author_name REGEXP ?)", [self.name.regex], [], False | |||||
cond = "(MATCH(author_name) AGAINST (? IN BOOLEAN MODE))" | |||||
return cond, [self.name.string], [], True | |||||
class Date(_Node): | |||||
"""Represents a date node. | |||||
Searches in the codelet_date_created or codelet_date_modified fields. | |||||
""" | |||||
CREATE = 1 | |||||
MODIFY = 2 | |||||
BEFORE = 1 | |||||
AFTER = 2 | |||||
def __init__(self, type_, relation, date): | |||||
""" | |||||
:type type_: int (``CREATE`` or ``MODIFY``) | |||||
:type relation: int (``BEFORE``, ``AFTER``) | |||||
:type date: datetime.datetime | |||||
""" | |||||
self.type = type_ | |||||
self.relation = relation | |||||
self.date = date | |||||
def __repr__(self): | |||||
types = {self.CREATE: "CREATE", self.MODIFY: "MODIFY"} | |||||
relations = {self.BEFORE: "BEFORE", self.AFTER: "AFTER"} | |||||
tm = "Date({0}, {1}, {2})" | |||||
return tm.format(types[self.type], relations[self.relation], self.date) | |||||
def sortkey(self): | |||||
return self.date.strftime("%Y%m%d%H%M%S") | |||||
def parameterize(self, tables): | |||||
column = {self.CREATE: "codelet_date_created", | |||||
self.MODIFY: "codelet_date_modified"}[self.type] | |||||
op = {self.BEFORE: "<=", self.AFTER: ">="}[self.relation] | |||||
return "(" + column + " " + op + " ?)", [self.date], [], False | |||||
class Symbol(_Node): | |||||
"""Represents a symbol node. | |||||
Searches in symbol_type and symbol_name. | |||||
""" | |||||
ALL = -1 | |||||
FUNCTION = 0 | |||||
CLASS = 1 | |||||
VARIABLE = 2 | |||||
TYPES = {FUNCTION: "FUNCTION", CLASS: "CLASS", VARIABLE: "VARIABLE"} | |||||
TYPES_INV = ["functions", "classes", "variables"] | |||||
def __init__(self, type_, name): | |||||
""" | |||||
:type type_: int (``ALL``, ``FUNCTION``, ``CLASS``, etc.) | |||||
:type name: :py:class:`._Literal` | |||||
""" | |||||
self.type = type_ | |||||
self.name = name | |||||
def __repr__(self): | |||||
type_ = self.TYPES.get(self.type, "ALL") | |||||
return "Symbol({0}, {1})".format(type_, self.name) | |||||
def sortkey(self): | |||||
return self.name.sortkey() | |||||
def parameterize(self, tables): | |||||
tables |= {"code", "symbols"} | |||||
if isinstance(self.name, Regex): | |||||
cond, name = "symbol_name REGEXP ?", self.name.regex | |||||
else: | |||||
cond, name = "symbol_name = ?", self.name.string | |||||
if self.type == self.ALL: | |||||
types = ", ".join(str(type_) for type_ in self.TYPES) | |||||
cond += " AND symbol_type IN (%s)" % types | |||||
if self.type != self.ALL: | |||||
cond += " AND symbol_type = %d" % self.type | |||||
return "(" + cond + ")", [name], [], False | |||||
class BinaryOp(_Node): | |||||
"""Represents a relationship between two nodes: ``and``, ``or``.""" | |||||
AND = object() | |||||
OR = object() | |||||
OPS = {AND: "AND", OR: "OR"} | |||||
def __init__(self, left, op, right): | |||||
self.left = left | |||||
self.op = op | |||||
self.right = right | |||||
def __repr__(self): | |||||
tmpl = "BinaryOp({0}, {1}, {2})" | |||||
return tmpl.format(self.left, self.OPS[self.op], self.right) | |||||
def sortkey(self): | |||||
return self.left.sortkey() + self.right.sortkey() | |||||
def parameterize(self, tables): | |||||
lcond, largs, lranks, need_lranks = self.left.parameterize(tables) | |||||
rcond, rargs, rranks, need_rranks = self.right.parameterize(tables) | |||||
lranks, rranks = lranks or [lcond], rranks or [rcond] | |||||
op = self.OPS[self.op] | |||||
cond = "(" + lcond + " " + op + " " + rcond + ")" | |||||
need_ranks = need_lranks or need_rranks or self.op == self.OR | |||||
return cond, largs + rargs, lranks + rranks, need_ranks | |||||
class UnaryOp(_Node): | |||||
"""Represents a transformation applied to one node: ``not``.""" | |||||
NOT = object() | |||||
OPS = {NOT: "NOT"} | |||||
def __init__(self, op, node): | |||||
self.op = op | |||||
self.node = node | |||||
def __repr__(self): | |||||
return "UnaryOp({0}, {1})".format(self.OPS[self.op], self.node) | |||||
def sortkey(self): | |||||
return self.node.sortkey() | |||||
def parameterize(self, tables): | |||||
cond, args, ranks, need_ranks = self.node.parameterize(tables) | |||||
new_cond = "(" + self.OPS[self.op] + " " + cond + ")" | |||||
ranks = ranks or [cond] | |||||
return new_cond, args, ranks, need_ranks |
@@ -0,0 +1,69 @@ | |||||
__all__ = ["Tree"] | |||||
QUERY_TEMPLATE = """SELECT codelet_id, (codelet_rank%s) AS score | |||||
FROM codelets %s | |||||
WHERE %s | |||||
GROUP BY codelet_id | |||||
ORDER BY score DESC | |||||
LIMIT %d OFFSET %d""".replace("\n", " ") | |||||
class Tree(object): | |||||
"""Represents a query tree.""" | |||||
def __init__(self, root): | |||||
self._root = root | |||||
def __repr__(self): | |||||
return "Tree({0})".format(self._root) | |||||
@property | |||||
def root(self): | |||||
"""The root node of the tree.""" | |||||
return self._root | |||||
def sortkey(self): | |||||
"""Return a string sort key for the query tree.""" | |||||
return self._root.sortkey() | |||||
def serialize(self): | |||||
"""Create a string representation of the query for caching. | |||||
:return: Query string representation. | |||||
:rtype: str | |||||
""" | |||||
return repr(self) | |||||
def build_query(self, page=1, page_size=10): | |||||
"""Convert the query tree into a parameterized SQL SELECT statement. | |||||
:param page: The page number to get results for. | |||||
:type page: int | |||||
:param page_size: The number of results per page. | |||||
:type page_size: int | |||||
:return: SQL query data. | |||||
:rtype: 2-tuple of (SQL statement string, query parameter tuple) | |||||
""" | |||||
def get_table_joins(tables): | |||||
data = [ | |||||
("code", "codelet_code_id", "code_id"), | |||||
("authors", "author_codelet", "codelet_id"), | |||||
("symbols", "symbol_code", "code_id") | |||||
] | |||||
tmpl = "INNER JOIN %s ON %s = %s" | |||||
for args in data: | |||||
if args[0] in tables: | |||||
yield tmpl % args | |||||
tables = set() | |||||
cond, arglist, ranks, need_ranks = self._root.parameterize(tables) | |||||
ranks = ranks or [cond] | |||||
if need_ranks: | |||||
score = " + ((%s) / %d)" % (" + ".join(ranks), len(ranks)) | |||||
else: | |||||
score = "" | |||||
joins = " ".join(get_table_joins(tables)) | |||||
offset = (page - 1) * page_size | |||||
query = QUERY_TEMPLATE % (score, joins, cond, page_size, offset) | |||||
return query, tuple(arglist * 2 if need_ranks else arglist) |
@@ -6,7 +6,8 @@ setup( | |||||
packages = find_packages(), | packages = find_packages(), | ||||
install_requires = [ | install_requires = [ | ||||
"Flask>=0.10.1", "pygments>=1.6", "requests>=2.2.0", | "Flask>=0.10.1", "pygments>=1.6", "requests>=2.2.0", | ||||
"beautifulsoup4>=3.2.1", "oursql>=0.9.3.1", "mmh3>=2.3"], | |||||
"beautifulsoup4>=3.2.1", "oursql>=0.9.3.1", "mmh3>=2.3", | |||||
"python-dateutil>=2.2"], | |||||
author = "Benjamin Attal, Ben Kurtovic, Severyn Kozak", | author = "Benjamin Attal, Ben Kurtovic, Severyn Kozak", | ||||
license = "MIT", | license = "MIT", | ||||
url = "https://github.com/earwig/bitshift" | url = "https://github.com/earwig/bitshift" | ||||
@@ -10,7 +10,6 @@ var searchBar = $("form#search-bar input[type='text']")[0]; | |||||
var resultsDiv = $("div#results")[0]; | var resultsDiv = $("div#results")[0]; | ||||
var typingTimer, lastValue; | var typingTimer, lastValue; | ||||
/* | /* | ||||
* Set all page callbacks. | * Set all page callbacks. | ||||
*/ | */ | ||||
@@ -43,10 +42,32 @@ var typingTimer, lastValue; | |||||
event.preventDefault(); | event.preventDefault(); | ||||
return false; | return false; | ||||
}); | }); | ||||
searchBar.onkeyup = typingTimer; | searchBar.onkeyup = typingTimer; | ||||
}()); | }()); | ||||
//Obtained by parsing python file with pygments | |||||
var codeExample = '<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre> 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40</pre></div></td><td class="code"><div class="hll"><pre><span class="sd">"""</span>\n<span class="sd">Module to contain all the project's Flask server plumbing.</span>\n<span class="sd">"""</span>\n\n<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>\n<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">render_template</span><span class="p">,</span> <span class="n">session</span>\n\n<span class="kn">from</span> <span class="nn">bitshift</span> <span class="kn">import</span> <span class="n">assets</span>\n<span class="c"># from bitshift.database import Database</span>\n<span class="c"># from bitshift.query import parse_query</span>\n\n<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>\n<span class="n">app</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">from_object</span><span class="p">(</span><span class="s">"bitshift.config"</span><span class="p">)</span>\n\n<span class="n">app_env</span> <span class="o">=</span> <span class="n">app</span><span class="o">.</span><span class="n">jinja_env</span>\n<span class="n">app_env</span><span class="o">.</span><span class="n">line_statement_prefix</span> <span class="o">=</span> <span class="s">"="</span>\n<span class="n">app_env</span><span class="o">.</span><span class="n">globals</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">assets</span><span class="o">=</span><span class="n">assets</span><span class="p">)</span>\n\n<span class="c"># database = Database()</span>\n\n<span class="nd">@app.route</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span>\n<span class="k">def</span> <span class="nf">index</span><span class="p">():</span>\n <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">"index.html"</span><span class="p">)</span>\n\n<span class="nd">@app.route</span><span class="p">(</span><span class="s">"/search/<query>"</span><span class="p">)</span>\n<span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">):</span>\n <span class="c"># tree = parse_query(query)</span>\n <span class="c"># database.search(tree)</span>\n <span class="k">pass</span>\n\n<span class="nd">@app.route</span><span class="p">(</span><span class="s">"/about"</span><span class="p">)</span>\n<span class="k">def</span> <span class="nf">about</span><span class="p">():</span>\n <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">"about.html"</span><span class="p">)</span>\n\n<span class="nd">@app.route</span><span class="p">(</span><span class="s">"/developers"</span><span class="p">)</span>\n<span class="k">def</span> <span class="nf">developers</span><span class="p">():</span>\n <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">"developers.html"</span><span class="p">)</span>\n\n<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>\n <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>\n</pre></div>\n</td></tr></table>' | |||||
searchBar.onkeyup = typingTimer; | |||||
var testCodelet = { | |||||
'code_url': 'https://github.com/earwig/bitshift/blob/develop/app.py', | |||||
'filename': 'app.py', | |||||
'language': 'python', | |||||
'date_created': 'May 10, 2014', | |||||
'date_modified': '2 days ago', | |||||
'origin': ['GitHub', 'https://github.com', ''], | |||||
'authors': ['sevko', 'earwig'], | |||||
'html_code': codeExample | |||||
}; | |||||
// Enable infinite scrolling down the results page. | |||||
$(window).scroll(function() { | |||||
var searchField = $("div#search-field"); | |||||
if($(window).scrollTop() + $(window).height() == $(document).height() && searchField.hasClass('partly-visible')){ | |||||
loadMoreResults(); | |||||
} | |||||
}); | |||||
/* | /* | ||||
* Clear the existing timer and set a new one the the user types text into the | * Clear the existing timer and set a new one the the user types text into the | ||||
* search bar. | * search bar. | ||||
@@ -117,6 +138,98 @@ function populateResults(){ | |||||
} | } | ||||
/* | /* | ||||
* Create a result element based upon a codelet instance. | |||||
* | |||||
* @return {Element} The result element. | |||||
*/ | |||||
function createResult(codelet) { | |||||
//Level 1 | |||||
var newDiv = document.createElement("div"), | |||||
table = document.createElement("table"), | |||||
row = document.createElement("tr"); | |||||
//Level 2 | |||||
var displayInfo = document.createElement("div"), | |||||
sidebar = document.createElement("td"), | |||||
codeElt = document.createElement("td"), | |||||
displayButton = document.createElement("td"), | |||||
hiddenInfoContainer = document.createElement("td"), | |||||
hiddenInfo = document.createElement("div"); | |||||
//Level 3 | |||||
var title = document.createElement("span"), | |||||
site = document.createElement("span"), | |||||
dateModified = document.createElement("span"), | |||||
language = document.createElement("span"), | |||||
dateCreated = document.createElement("span"), | |||||
authors = document.createElement("div"); | |||||
//Classes and ID's | |||||
newDiv.classList.add('result'); | |||||
displayInfo.id = 'display-info'; | |||||
sidebar.id = 'sidebar'; | |||||
codeElt.id = 'code'; | |||||
displayButton.id = 'display-button'; | |||||
hiddenInfo.id = 'hidden-info'; | |||||
title.id = 'title'; | |||||
site.id = 'site'; | |||||
dateModified.id = 'date-modified'; | |||||
language.id = 'language'; | |||||
dateCreated.id = 'date-created'; | |||||
authors.id = 'authors'; | |||||
//Add the bulk of the html | |||||
title.innerHTML = 'File <a href="' + codelet.code_url + '">' | |||||
+ codelet.filename + '</a>'; | |||||
site.innerHTML = 'on <a href="' + codelet.origin[1] + '">' + codelet.origin[0] +'</a>'; | |||||
language.innerHTML = codelet.language; | |||||
dateModified.innerHTML = 'Last modified ' + codelet.date_modified; | |||||
// Needs to be changed from int to string on the server | |||||
dateCreated.innerHTML = 'Created ' + codelet.date_created; | |||||
authors.innerHTML = 'Authors: '; | |||||
$.each(codelet.authors, function(i, a) { | |||||
authors.innerHTML += '<a href=#>' + a + ' </a>'; | |||||
}); | |||||
sidebar.innerHTML = ''; | |||||
// Needs to be processed on the server | |||||
codeElt.innerHTML = '<div id=tablecontainer>' + codelet.html_code + '</div>'; | |||||
//Event binding | |||||
$(displayButton).hover(function(e) { | |||||
$(row).addClass('display-all'); | |||||
}); | |||||
$(newDiv).on('transitionend', function(e) { | |||||
$(newDiv).one('mouseleave', function(e) { | |||||
$(row).removeClass('display-all'); | |||||
}); | |||||
}); | |||||
//Finish and append elements to parent elements | |||||
hiddenInfo.appendChild(dateCreated); | |||||
hiddenInfo.appendChild(dateModified); | |||||
hiddenInfo.appendChild(authors); | |||||
hiddenInfoContainer.appendChild(hiddenInfo); | |||||
row.appendChild(sidebar); | |||||
row.appendChild(codeElt); | |||||
row.appendChild(hiddenInfoContainer); | |||||
row.appendChild(displayButton); | |||||
table.appendChild(row); | |||||
displayInfo.appendChild(title); | |||||
displayInfo.appendChild(site); | |||||
displayInfo.appendChild(language); | |||||
newDiv.appendChild(displayInfo); | |||||
newDiv.appendChild(table); | |||||
return newDiv; | |||||
} | |||||
/* | |||||
* AJAX the current query string to the server, and return its response. | * AJAX the current query string to the server, and return its response. | ||||
* | * | ||||
* @return {Array} The server's response in the form of `div.result` DOM | * @return {Array} The server's response in the form of `div.result` DOM | ||||
@@ -125,12 +238,7 @@ function populateResults(){ | |||||
function queryServer(){ | function queryServer(){ | ||||
var resultDivs = [] | var resultDivs = [] | ||||
for(var result = 0; result < 20; result++){ | for(var result = 0; result < 20; result++){ | ||||
var newDiv = document.createElement("div"); | |||||
newDiv.classList.add("result"); | |||||
newDiv.innerHTML = Math.random(); | |||||
newDiv.style.textAlign = "center"; | |||||
newDiv.style.color = "#" + Math.floor(Math.random() * | |||||
16777215).toString(16); | |||||
var newDiv = createResult(testCodelet); | |||||
resultDivs.push(newDiv); | resultDivs.push(newDiv); | ||||
} | } | ||||
@@ -6,6 +6,10 @@ | |||||
@import variables | @import variables | ||||
$minSearchFieldsWidth: 490px | $minSearchFieldsWidth: 490px | ||||
$resultWidth: 1000px | |||||
$sidebarWidth: 30px | |||||
$codeWidth: 650px | |||||
$hiddenInfoWidth: 250px | |||||
.ui-datepicker | .ui-datepicker | ||||
font-size: 70% | font-size: 70% | ||||
@@ -282,13 +286,124 @@ div#results | |||||
margin-right: auto | margin-right: auto | ||||
width: 80% | width: 80% | ||||
/* TODO: | |||||
1) Sidebar | |||||
- Add way to cycle through hits in the code. | |||||
2) Hidden info | |||||
- Add links for authors. | |||||
- Remove language field. | |||||
3) Header | |||||
- Add an icon for the website. | |||||
- Add language tag. | |||||
4) Code body | |||||
- Add highlighting. | |||||
5) Display button | |||||
- unicode glyph */ | |||||
div.result | div.result | ||||
background-color: #F8F8F8 | |||||
width: $resultWidth | |||||
height: 200px | |||||
margin-top: 2% | |||||
margin-bottom: 10% | margin-bottom: 10% | ||||
margin-top: 1% | |||||
padding: 1% | |||||
table | |||||
border-collapse: collapse | |||||
border: 1px solid $baseColor3 | |||||
height: inherit | |||||
tr | |||||
height: inherit | |||||
&.cascade | &.cascade | ||||
@extend .t3 | @extend .t3 | ||||
margin-bottom: 0% | |||||
div#display-info | |||||
font-size: 1.3em | |||||
padding: 5px 0px 5px 5px | |||||
border: 1px dotted $baseColor3 | |||||
border-bottom: none | |||||
width: 400px | |||||
a | |||||
text-decoration: none | |||||
&:hover | |||||
color: orange | |||||
#title | |||||
margin-right: 10px | |||||
#site | |||||
text-transform: capitalize | |||||
#language | |||||
font-size: 0.8em | |||||
font-weight: bold | |||||
margin-left: 100px | |||||
padding: 3px | |||||
@include vendor(border-radius, 2px) | |||||
background: #ddd | |||||
color: orange | |||||
td#sidebar | |||||
width: $sidebarWidth | |||||
background-color: #eee | |||||
border-right: 1px solid $baseColor3 | |||||
height: inherit | |||||
td#code | |||||
width: $codeWidth | |||||
height: inherit | |||||
border-right: 1px solid $baseColor3 | |||||
@include vendor(transition, width 0.2s ease-in-out) | |||||
.display-all & | |||||
width: 500px | |||||
#tablecontainer | |||||
overflow: scroll | |||||
width: 100% | |||||
height: inherit | |||||
background-color: #49483e | |||||
table | |||||
table-layout:fixed | |||||
border-collapse: collapse | |||||
border: none | |||||
font-family: monospace | |||||
td#display-button | |||||
width: 25px | |||||
background: url(https://cdn1.iconfinder.com/data/icons/windows-8-metro-style/512/View_Details-.png) | |||||
background-size: 25px 25px | |||||
background-repeat: no-repeat | |||||
background-position: center | |||||
.display-all & | |||||
@include vendor(transform, rotateY(180deg)) | |||||
div#hidden-info | |||||
width: $hiddenInfoWidth | |||||
margin-left: -$hiddenInfoWidth | |||||
height: 100% | |||||
padding-top: 40px | |||||
font-size: 1.2em | |||||
line-height: 1.5em | |||||
@include vendor(transition, margin-left 0.2s ease-in-out) | |||||
.display-all & | |||||
margin-left: 0px | |||||
padding-left: 20px | |||||
#date-created | |||||
display: inline-block | |||||
#date-modified | |||||
display: block | |||||
#authors | |||||
a | |||||
text-decoration: none | |||||
&:hover | |||||
color: orange |
@@ -9,9 +9,7 @@ html, body | |||||
height: 100% | height: 100% | ||||
margin: 0 | margin: 0 | ||||
padding: 0 | padding: 0 | ||||
* | |||||
font-family: sans-serif | |||||
font-family: sans-serif | |||||
div#container | div#container | ||||
min-height: 100% | min-height: 100% | ||||
@@ -8,6 +8,7 @@ | |||||
{{ assets.tag("lib/jqueryui.custom.min.css") }} | {{ assets.tag("lib/jqueryui.custom.min.css") }} | ||||
{{ assets.tag("lib/jquery.min.js") }} | {{ assets.tag("lib/jquery.min.js") }} | ||||
{{ assets.tag("lib/jquery-ui.min.js") }} | {{ assets.tag("lib/jquery-ui.min.js") }} | ||||
{{ assets.tag("lib/highlight.css") }} | |||||
{{ assets.tag("index.css") }} | {{ assets.tag("index.css") }} | ||||
@@ -88,6 +89,31 @@ | |||||
<span class="regex">Regex</span> | <span class="regex">Regex</span> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div id="lower-half"> | |||||
<ul> | |||||
<li> | |||||
<label for="symbols">Symbols</label> | |||||
<input type="text" name="symbols" id="symbols"><br> | |||||
</li> | |||||
<li> | |||||
<label for="functions">Functions</label> | |||||
<input type="text" name="functions" id="functions"><br> | |||||
</li> | |||||
<li> | |||||
<label for="classes">Classes</label> | |||||
<input type="text" name="classes" id="classes"><br> | |||||
</li> | |||||
<li> | |||||
<label for="variables">Variables</label> | |||||
<input type="text" name="variables" id="variables"><br> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</form> | </form> | ||||
@@ -0,0 +1,67 @@ | |||||
from __future__ import unicode_literals | |||||
import unittest | |||||
from bitshift.query import parse_query | |||||
TESTS = [ | |||||
# Text | |||||
("test", "Tree(Text(String(u'test')))"), | |||||
("re:test", "Tree(Text(Regex(u'test')))"), | |||||
# Language | |||||
("language:python", "Tree(Language(Python))"), | |||||
("language:py", "Tree(Language(Python))"), | |||||
("l:r:r..y", "Tree(Language(Ruby))"), | |||||
("lang:re:py|c", "Tree(BinaryOp(Language(C), OR, Language(Python)))"), | |||||
# Author | |||||
('"author:Ben Kurtovic"', "Tree(Author(String(u'Ben Kurtovic')))"), | |||||
(r"'a:re:b.*?\sk.*?'", r"Tree(Author(Regex(u'b.*?\\sk.*?')))"), | |||||
# Date | |||||
("'create:before:Jan 1, 2014'", | |||||
"Tree(Date(CREATE, BEFORE, 2014-01-01 00:00:00))"), | |||||
("'modify:after:2010-05-09 10:11:12'", | |||||
"Tree(Date(MODIFY, AFTER, 2010-05-09 10:11:12))"), | |||||
# Symbol | |||||
("sym:foobar", "Tree(Symbol(ALL, String(u'foobar')))"), | |||||
("func:foo_bar", "Tree(Symbol(FUNCTION, String(u'foo_bar')))"), | |||||
("func:foo_bar()", "Tree(Symbol(FUNCTION, String(u'foo_bar')))"), | |||||
("class:FooBar", "Tree(Symbol(CLASS, String(u'FooBar')))"), | |||||
("var:foobar", "Tree(Symbol(VARIABLE, String(u'foobar')))"), | |||||
("var:r:foobar", "Tree(Symbol(VARIABLE, Regex(u'foobar')))"), | |||||
# Composition | |||||
("(a and b) or (c and d)", ", ".join([ | |||||
"Tree(BinaryOp(BinaryOp(Text(String(u'a'))", "AND", | |||||
"Text(String(u'b')))", "OR", "BinaryOp(Text(String(u'c'))", "AND", | |||||
"Text(String(u'd')))))"])), | |||||
("a and b or c and d", ", ".join([ | |||||
"Tree(BinaryOp(BinaryOp(Text(String(u'a'))", "AND", | |||||
"Text(String(u'b')))", "OR", "BinaryOp(Text(String(u'c'))", "AND", | |||||
"Text(String(u'd')))))"])), | |||||
("a and b or c or d", ", ".join([ | |||||
"Tree(BinaryOp(BinaryOp(Text(String(u'a'))", "AND", | |||||
"Text(String(u'b')))", "OR", "BinaryOp(Text(String(u'c'))", "OR", | |||||
"Text(String(u'd')))))"])), | |||||
("a and (b or c or d)", ", ".join([ | |||||
"Tree(BinaryOp(Text(String(u'a'))", "AND", | |||||
"BinaryOp(Text(String(u'b'))", "OR", "BinaryOp(Text(String(u'c'))", "OR", | |||||
"Text(String(u'd'))))))"])), | |||||
("a not b", ", ".join([ | |||||
"Tree(BinaryOp(Text(String(u'a'))", "AND", "UnaryOp(NOT", | |||||
"Text(String(u'b')))))"])), | |||||
] | |||||
class TestQueryParser(unittest.TestCase): | |||||
"""Unit tests for the query parser in :py:mod:`bitshift.query`.""" | |||||
def test_parse(self): | |||||
"""test full query parsing""" | |||||
for test, expected in TESTS: | |||||
self.assertEqual(expected, parse_query(test).serialize()) | |||||
if __name__ == "__main__": | |||||
unittest.main(verbosity=2) |