Skip to content

Commit 6ff06a6

Browse files
committed
merge: resolve conflicts and merge include_public_api_only into main
- Resolve merge conflict in pylibfinder_tui.py by using Score field instead of Similarity - Keep better error handling and threshold validation from include_public_api_only branch - Apply defensive programming with .get() for missing Score keys - Merge features: type detection, private API filtering, and TUI - Fixes #3 (include public APIs only) and #4 (type information)
2 parents 5a008af + a7a033a commit 6ff06a6

File tree

6 files changed

+105
-10
lines changed

6 files changed

+105
-10
lines changed

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ Example
9696
>>> pylibfinder.find_similar('parseInt', 0.6)
9797
[{'Module': 're._parser', 'Function': '_parse_sub', 'Similarity': 0.6}]
9898
>>>
99+
>>> # Include private APIs (functions starting with _) in search results
100+
>>> pylibfinder.find_similar('parse', 0.5, include_private=True)
101+
[{'Module': 'builtins', 'Function': 'compile', 'Similarity': 0.5}, {'Module': 're._parser', 'Function': '_parse_sub', 'Similarity': 0.8}, ...]
102+
>>>
99103
100104
101105
@@ -142,6 +146,7 @@ Then launch the interactive TUI:
142146
- ``power`` - Find power-related functions
143147
- ``print 0.8`` - Find functions similar to "print" with high confidence
144148
- ``parseInt 0.6`` - Find Java-style parseInt alternatives
149+
- ``parse 0.5 true`` - Find functions similar to "parse" including private APIs
145150

146151

147152
Contributing

pylibfinder_tui.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ class SearchInput(Static):
2929
"""Search input area"""
3030

3131
def compose(self) -> ComposeResult:
32-
yield Input(placeholder="Enter keyword (e.g., power, print, parseInt)", id="search-input")
32+
yield Input(
33+
placeholder="Enter keyword (e.g., power, print, parseInt) with optional threshold and include_private flag",
34+
id="search-input",
35+
)
3336

3437

3538
class SearchApp(App):
@@ -106,16 +109,28 @@ def perform_search(self, event: Input.Submitted) -> None:
106109
parts = query.rsplit(" ", 1)
107110
keyword = parts[0]
108111
threshold = 0.5
112+
include_private = False
109113

110114
try:
111115
if len(parts) == 2:
112116
threshold = float(parts[1])
113-
except ValueError:
117+
keyword = parts[0]
118+
# Validate threshold
119+
if threshold < 0 or threshold > 1:
120+
self.notify("Threshold must be between 0 and 1", severity="warning")
121+
return
122+
123+
if len(parts) > 2:
124+
include_private = parts[2].lower() in ["true", "1", "yes"]
125+
except Exception:
114126
pass
115127

128+
if not keyword:
129+
return
130+
116131
# Perform search
117132
try:
118-
results = pylibfinder.find_similar(keyword, threshold)
133+
results = pylibfinder.find_similar(keyword, threshold, include_private=include_private)
119134
self.display_results(results, keyword, threshold)
120135
except Exception as e:
121136
self.display_error(str(e))
@@ -128,14 +143,14 @@ def display_results(self, results: list[dict], query: str, threshold: float) ->
128143
if not results:
129144
return
130145

131-
# Sort by similarity descending
132-
sorted_results = sorted(results, key=lambda x: x["Similarity"], reverse=True)
146+
# Sort by score descending (handle missing Score key)
147+
sorted_results = sorted(results, key=lambda x: x.get("Score", 0), reverse=True)
133148

134149
# Add rows to table
135150
for idx, result in enumerate(sorted_results, 1):
136151
func_name = result["Function"]
137152
module_name = result["Module"]
138-
score = result["Similarity"]
153+
score = result.get("Score", 0)
139154
percentage = f"{score*100:.1f}%"
140155

141156
# Determine color based on score

src/module_scanner.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,17 @@ PyObject* create_match_dict(const char *module_name,
6060

6161
return dict_item;
6262
}
63+
64+
// Helper to create a result dictionary with type information
65+
PyObject* create_match_dict_with_type(const char *module_name,
66+
const char *function_name,
67+
double score,
68+
int is_type) {
69+
PyObject *dict_item = create_match_dict(module_name, function_name, score);
70+
if (!dict_item) return NULL;
71+
72+
PyObject *type_obj = is_type ? Py_True : Py_False;
73+
PyDict_SetItemString(dict_item, "Type", type_obj);
74+
75+
return dict_item;
76+
}

src/module_scanner.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@ PyObject* create_match_dict(const char *module_name,
2424
const char *function_name,
2525
double score);
2626

27+
// Helper to create a result dictionary with type information
28+
PyObject* create_match_dict_with_type(const char *module_name,
29+
const char *function_name,
30+
double score,
31+
int is_type);
32+
2733
#endif

src/pylibfinder.c

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
/* ========== SIMILARITY MATCHING ========== */
88

9+
static int is_public_api(const char *function_name) {
10+
return function_name && function_name[0] != '_';
11+
}
12+
913
static int levenshtein_distance(const char *s1, const char *s2) {
1014
int len1 = strlen(s1);
1115
int len2 = strlen(s2);
@@ -85,6 +89,8 @@ static int has_substring_match(const char *query, const char *func_name) {
8589
typedef struct {
8690
const char *query;
8791
double threshold;
92+
int include_private;
93+
PyObject *modules_dict; // for checking if items are types
8894
} SimilarityContext;
8995

9096
static int similarity_processor(const char *module_name,
@@ -93,13 +99,32 @@ static int similarity_processor(const char *module_name,
9399
void *user_data) {
94100
SimilarityContext *ctx = (SimilarityContext *)user_data;
95101

102+
// Skip private functions (those starting with underscore) unless include_private is set
103+
if (!ctx->include_private && !is_public_api(function_name)) {
104+
return 0;
105+
}
106+
96107
double similarity = calculate_similarity(ctx->query, function_name);
97108
if (has_substring_match(ctx->query, function_name)) {
98109
similarity = (similarity + 1.0) / 2.0;
99110
}
100111

101112
if (similarity >= ctx->threshold) {
102-
PyObject *dict_item = create_match_dict(module_name, function_name, similarity);
113+
// Check if the item is a type
114+
int is_type = 0;
115+
if (ctx->modules_dict != NULL) {
116+
PyObject *module = PyDict_GetItemString(ctx->modules_dict, module_name);
117+
if (module != NULL) {
118+
PyObject *item = PyObject_GetAttrString(module, function_name);
119+
if (item != NULL) {
120+
is_type = PyType_Check(item);
121+
Py_DECREF(item);
122+
}
123+
PyErr_Clear(); // Clear any errors from GetAttr
124+
}
125+
}
126+
127+
PyObject *dict_item = create_match_dict_with_type(module_name, function_name, similarity, is_type);
103128
if (dict_item) {
104129
PyList_Append(result_list, dict_item);
105130
Py_DECREF(dict_item);
@@ -109,11 +134,14 @@ static int similarity_processor(const char *module_name,
109134
return 0;
110135
}
111136

112-
static PyObject* find_similar(PyObject* self, PyObject* args) {
137+
static PyObject* find_similar(PyObject* self, PyObject* args, PyObject* kwargs) {
113138
const char *keyword;
114139
double threshold = 0.5;
140+
int include_private = 0;
115141

116-
if (!PyArg_ParseTuple(args, "s|d", &keyword, &threshold)) {
142+
static char *kwlist[] = {"keyword", "threshold", "include_private", NULL};
143+
144+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|dp", kwlist, &keyword, &threshold, &include_private)) {
117145
return NULL;
118146
}
119147

@@ -122,9 +150,13 @@ static PyObject* find_similar(PyObject* self, PyObject* args) {
122150
SimilarityContext ctx;
123151
ctx.query = keyword;
124152
ctx.threshold = threshold;
153+
ctx.include_private = include_private;
154+
ctx.modules_dict = NULL; // Initialize to NULL
125155

126156
// Direct module scanning to pass context properly
127157
PyObject *modules_dict = PyImport_GetModuleDict();
158+
ctx.modules_dict = modules_dict; // Assign to context
159+
128160
Py_ssize_t pos = 0;
129161
PyObject *key, *value;
130162

@@ -160,7 +192,7 @@ static PyObject* find_similar(PyObject* self, PyObject* args) {
160192
/* ========== MODULE DEFINITION ========== */
161193

162194
static PyMethodDef module_methods[] = {
163-
{"find_similar", find_similar, METH_VARARGS, "Find similar functions in Python stdlib using semantic similarity. Optional threshold parameter (0.0-1.0, default 0.5)."},
195+
{"find_similar", (PyCFunction)find_similar, METH_VARARGS | METH_KEYWORDS, "Find similar functions in Python stdlib using semantic similarity. Optional threshold parameter (0.0-1.0, default 0.5) and include_private parameter (default False)."},
164196
{NULL, NULL, 0, NULL}
165197
};
166198

tests/test_pylibfinder.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,26 @@ def test_find_similar_substring_match():
8080
break
8181

8282
assert found_print_related
83+
84+
85+
def test_find_similar_excludes_private_by_default():
86+
"""Test that private functions (starting with _) are excluded by default."""
87+
result = pylibfinder.find_similar("parse", 0.5)
88+
89+
# Assert that all results are public functions (don't start with _)
90+
for item in result:
91+
assert not item["Function"].startswith("_")
92+
93+
94+
def test_find_similar_includes_private_when_flag_set():
95+
"""Test that private functions are included when include_private=True."""
96+
result_without_private = pylibfinder.find_similar("parse", 0.5, include_private=False)
97+
result_with_private = pylibfinder.find_similar("parse", 0.5, include_private=True)
98+
99+
# Assert that include_private=True returns more or equal results
100+
assert len(result_with_private) >= len(result_without_private)
101+
102+
# If there are more results, at least some should be private
103+
if len(result_with_private) > len(result_without_private):
104+
has_private = any(item["Function"].startswith("_") for item in result_with_private)
105+
assert has_private

0 commit comments

Comments
 (0)