├── .gitignore ├── LICENSE.txt ├── README.md ├── funcfinder ├── __init__.py ├── __main__.py ├── _imports.py ├── answers │ ├── __init__.py │ ├── decorator.py │ ├── dict.py │ ├── list.py │ ├── math.py │ └── string.py ├── questions │ ├── __init__.py │ ├── decorator.py │ ├── dict.py │ ├── list.py │ ├── math.py │ └── string.py └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | sandbox.py 61 | .idea/ 62 | gh-md-toc -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Hall 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # funcfinder 2 | 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/alexmojaki/funcfinder?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | 5 | funcfinder is a new way to solve problems of the form "I need a Python function that does X". This is commonly solved using search engines and forums where the results exist in the form of mere text. Some of it may represent code, but it can't be executed. funcfinder is a repository of 'questions', which are parametrised unit tests, and 'answers', which are functions that satisfy the tests. By using actual code, new possibilities open up: 6 | 7 | * Answers to questions are essentially guaranteed to be correct because they are unit tested. 8 | * Questions can have multiple answers, meaning the same unit test is applied to different functions, which has several implications: 9 | * Unit testing code is effectively reused. 10 | * Every answer is timed automatically to show performance differences. 11 | * You can 'try out' multiple answers simultaneously by passing sample input to them all and seeing how the results subtly differ. 12 | * You can easily switch between answers with reduced risk of breaking your code, e.g. if: 13 | * It turns out an answer has a bug. 14 | * Someones creates a new answer which is more efficient. 15 | * You find you have more specific requirements, e.g. you need a function that handles any iterable, not just lists. 16 | * New questions can be asked using only code by giving a simple test with a sample of what the function should do. For example, `assert func(["a", "bb", "c"], len) == {1: ["a", "c"], 2: ["bb"]}` describes a function which groups elements of an iterable into a dictionary based on some key function. This little test is run against the entire repository of functions to find an answer automatically. 17 | * Questions can depend on other questions through simple function calls (more code reuse!) which are detected automatically. Since this is expressed in code, various complex relationships are possible, but the most common case is an inheritance hierarchy of requirements for answers to satisfy. This lets you easily navigate from functions with loose requirements to functions with more specific ones. 18 | * Answers can also use each other and again this can be detected automatically. The result is that: 19 | * Answers to complex questions are easier to write because a whole repository of generically useful functions is available. 20 | * Answers are cleanly divided into small functions with different levels of abstraction, resulting in more readable code. 21 | * In the process of finding a function to solve one specific problem you can find more generic functions which you are likely to use elsewhere. 22 | * By not relying on natural language descriptions: 23 | * There is no doubt or ambiguity in what is being asked for, or what the answer accomplishes. 24 | * There is no need to know a particular language (e.g. English) to ask a question, answer it, or to understand both the question and answer. 25 | 26 | This repository is a basic implementation of this idea providing the most essential features. It's an experiment to see if people are interested and willing to contribute questions and answers. If so, it may be turned into a complete website, which could ultimately mean answers appearing directly in Google searches. Imagine having all of the above, with no installation effort, a fancy frontend, maybe even integration with Stack Overflow. Imagine having implementations for other languages. If this excites you, read on. 27 | 28 | ## Table of Contents 29 | 30 | * [Getting started](#getting-started) 31 | * [Usage](#usage) 32 | * [Searching](#searching) 33 | * [Showing questions](#showing-questions) 34 | * [The question](#the-question) 35 | * [Answers](#answers) 36 | * [Dependencies](#dependencies) 37 | * [Asking questions](#asking-questions) 38 | * [Contributing](#contributing) 39 | * [Folder structure](#folder-structure) 40 | * [Naming](#naming) 41 | * [Imports](#imports) 42 | * [Writing questions](#writing-questions) 43 | * [Writing answers](#writing-answers) 44 | * [Answers ignored when asking](#answers-ignored-when-asking) 45 | * [FAQ](#faq) 46 | 47 | ## Getting started 48 | 49 | Fork this repository, clone your fork, then run 50 | 51 | `python setup.py develop` 52 | 53 | within the main directory. This will install the library so that you can use it in Python scripts anywhere, and also ensure that changes in the repository (whether you make them or you pull in remote updates) are immediately reflected in scripts. It also installs the shell command `funcfinder`. 54 | 55 | ## Usage 56 | 57 | ### Searching 58 | 59 | Finding a function typically begins with a simple keyword search. The `funcfinder` shell command is made for this. By itself, or with the flag `-h`, it will give you some help on usage in case you get lost. To search for a function, use the `find` subcommand. This takes any number of positional arguments representing search terms. The results are questions (answers come later) whose docstrings contain all the search terms directly, ignoring case. You can use quotes to force terms to appear together. 60 | 61 | For example, let's say we want a dictionary where the keys are in sorted order. This might go like this: 62 | 63 | ``` 64 | $ funcfinder find dict sort 65 | Searching for the terms ['dict', 'sort']... 66 | 67 | sort_dict_by_key: 68 | 69 | Return a copy of a dict which still supports all the standard operations with the usual API, 70 | plus can be iterated over in sorted order by key. Adding keys to the dict may not necessarily preserve this order - 71 | for that, see always_sorted_dict_by_key. 72 | 73 | ----------------------- 74 | 75 | sort_dict_by_value: 76 | 77 | Return a copy of a dict which still supports all the standard operations with the usual API, 78 | plus can be iterated over in sorted order by value. Adding keys to the dict may not necessarily preserve this order. 79 | 80 | ----------------------- 81 | 82 | always_sorted_dict_by_key: 83 | 84 | Return a copied dict sorted by key which preserves its order upon updates. 85 | 86 | ----------------------- 87 | ``` 88 | 89 | Here we see the names and docstrings of all the questions that satisfied the search. 90 | 91 | ### Showing questions 92 | 93 | Suppose that `sort_dict_by_key` sounds most like what we want. We can take a closer look using the `show` subcommand. An example is below. This looks like a lot to absorb, but most of it is source code for the various pieces involved, along with where to find it. This is great for when you actually use the tool, but understanding it all is not required for this tutorial. We'll walk through it. 94 | 95 | ``` 96 | $ funcfinder show sort_dict_by_key 97 | 98 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/questions/dict.py : 51 99 | def sort_dict_by_key(func): 100 | """ 101 | Return a copy of a dict which still supports all the standard operations with the usual API, 102 | plus can be iterated over in sorted order by key. Adding keys to the dict may not necessarily preserve this order - 103 | for that, see always_sorted_dict_by_key. 104 | """ 105 | copy_dict(func) 106 | 107 | # On my machine at least, this dict does not look sorted 108 | original_dict = {'a': 0, 's': 1, 'd': 2, 'f': 3} 109 | sorted_dict = func(original_dict) 110 | 111 | # Iteration is now ordered 112 | assertEqual(sorted_dict.items(), [('a', 0), ('d', 2), ('f', 3), ('s', 1)]) 113 | 114 | # Larger test 115 | sorted_keys = list(itertools.product(string.ascii_lowercase, string.ascii_lowercase)) 116 | shuffled_keys = list(sorted_keys) 117 | for i in xrange(10): 118 | random.shuffle(shuffled_keys) 119 | original_dict = dict(itertools.izip(shuffled_keys, itertools.count())) 120 | sorted_dict = func(original_dict) 121 | assertEqualIters(sorted_keys, sorted_dict.iterkeys()) 122 | 123 | Answers: 124 | 125 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/answers/dict.py : 14 126 | def ordered_dict_sorted_by_key(d): 127 | return collections.OrderedDict(sorted(d.items())) 128 | 129 | Passed tests successfully. 130 | -------------------------- 131 | 132 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/answers/dict.py : 19 133 | def sorted_dict(d): 134 | return sortedcontainers.SortedDict(d) 135 | 136 | Failed tests with exception: 137 | TryImportError: No module named sortedcontainers 138 | 139 | Dependencies: 140 | 141 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/questions/dict.py : 22 142 | def copy_dict(func): 143 | """ 144 | Returns a new separate dict equal to the original. Updates to the copy don't affect the original. 145 | """ 146 | original = {'a': 1, 'd': 2, 'b': 3, 'c': 4} 147 | copy = func(original) 148 | 149 | # An equal but separate copy has been made 150 | assertEqual(original, copy) 151 | assertIsNot(original, copy) 152 | 153 | # Usual key access still works 154 | assertEqual(copy['d'], 2) 155 | 156 | # Deletion works 157 | del copy['d'] 158 | assertIsNone(copy.get('d')) 159 | 160 | # But it doesn't delete the key in the original 161 | assertEqual(original['d'], 2) 162 | 163 | # Insertion works 164 | copy['x'] = 5 165 | assertEqual(copy['x'], 5) 166 | 167 | # And again, doesn't affect the original 168 | assertIsNone(original.get('x')) 169 | ``` 170 | 171 | #### The question 172 | 173 | The first thing in the output is the source code of the question. A question is a function which takes a single argument, also a function, traditionally named `func`. `func` is a potential answer to the question: the question will call it with whatever arguments it wants and make assertions about the results. `func` is considered a correct solution if the whole question can execute without any errors. So a question is just a unit test that tests a single function. By reading it you can be confident about what the answer(s) will provide. 174 | 175 | #### Answers 176 | 177 | Next we see answers to the question that have been marked as solutions. These are immediately tested against the question to make sure they work. Indeed, the second of the two answers failed with an exception! Normally we would see a traceback, but this is a special case: a `TryImportError` just indicates that you're missing some required library to use this answer. Questions and answers can freely use any third party libraries and nothing will go wrong if you don't have them installed. They just have to be imported slightly differently. 178 | 179 | [sortedcontainers](http://www.grantjenks.com/docs/sortedcontainers/) is a potentially useful library and can easily be installed using `pip`. Suppose we install it. Now the answer passes the tests defined by the question, and we also get something extra: 180 | 181 | ``` 182 | ... 183 | 184 | Answers: 185 | 186 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/answers/dict.py : 14 187 | def ordered_dict_sorted_by_key(d): 188 | return collections.OrderedDict(sorted(d.items())) 189 | 190 | Passed tests successfully. 191 | -------------------------- 192 | 193 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/answers/dict.py : 19 194 | def sorted_dict(d): 195 | return sortedcontainers.SortedDict(d) 196 | 197 | Passed tests successfully. 198 | -------------------------- 199 | 200 | Best times per answer: 201 | ordered_dict_sorted_by_key: 1.692 s 202 | sorted_dict: 0.587 s 203 | (among 5 sets of 64 repetitions) 204 | 205 | ... 206 | ``` 207 | 208 | Whenever a question has more than one correct answer, they are automatically timed. Now we can see that sortedcontainers is significantly faster than the standard library solution. 209 | 210 | #### Dependencies 211 | 212 | The last part of the output shows dependencies, which are other questions or answers that were called when running the test. This means that questions and answers can be reused by authors freely, while users still get to see all the relevant source code. 213 | 214 | In this case the `sort_dict_by_key` question directly tests properties related to order, but it also has the requirement that the dictionary it returns is a new, separate copy of the original. This requirement is expressed in the first line with the statement `copy_dict(func)`. This does not mean that a dictionary `func` is being copied, but that the answer `func` must also solve the question `copy_dict`. `copy_dict` itself is not a difficult question - the method `dict.copy()` can solve that - but it is a common requirement for other questions. `sort_dict_by_value` is an example of another question that reuses `copy_dict`. 215 | 216 | The function call `copy_dict(func)` is all that is needed from the question author. funcfinder picks it up automatically and prints the source of `copy_dict` at the end so that users can immediately see the additional requirements imposed by the `sort_dict_by_key` question. The same goes for answers. For example, the `is_even` answer shown in the next section uses the `is_divisible_by` answer, so that is printed out as well. 217 | 218 | ### Asking questions 219 | 220 | If you can't find a question by searching normally then it's time to write a question in code. Here's a simple example: 221 | 222 | ``` 223 | import funcfinder 224 | 225 | def how_to_check_if_number_is_even(func): 226 | assert func(2) 227 | assert not func(3) 228 | assert func(4) 229 | 230 | funcfinder.ask(how_to_check_if_number_is_even) 231 | ``` 232 | 233 | If you're still confused about what a question is, read [here](#the-question). The output of running this script is (roughly): 234 | 235 | ``` 236 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/answers/math.py : 11 237 | def is_even(a): 238 | return is_divisible_by(a, 2) 239 | 240 | Solves the question is_even 241 | 242 | ------------------------- 243 | 244 | Dependencies: 245 | 246 | /Users/alexhall/Dropbox/python/funcfinder/funcfinder/answers/math.py : 6 247 | def is_divisible_by(a, b): 248 | return a % b == 0 249 | ``` 250 | 251 | Note that the question does not need to thoroughly test the function. Just give enough detail to narrow things down. Most answers in the repository won't even expect an integer as input and will fail immediately. A few unwanted answers could potentially survive this test (e.g. check if the number is a power of two), but it's very easy to either take a quick look and see which answer you actually need, or to add a couple more test cases to narrow things down (e.g. `assert func(6)`). 252 | 253 | If answers are found they will come with names of questions that they solve, which you can inspect with `funcfinder show` to see more detailed tests. 254 | 255 | There are just a few simple guidelines to asking questions: 256 | 257 | Every call to `func` in the question must have the same number of arguments, none of them named. You're looking for a solution to a specific problem, not a neat API. 258 | 259 | Keep in mind that your question is going to run a large number of times. Keep the inputs small: small numbers, short lists, etc. Definitely don't pass any infinite iterators. Call the given function as soon as possible so that it can fail quickly for wrong answers. If your question involves something even slightly slow such as setting up a database connection or opening a file, try to do it once outside the question definition. This is safe as answers are not allowed to modify these resources (see below), but you should still reset iterators and database cursors and seek to the beginning of files at the start of the question. 260 | 261 | Some answers will be marked to say that they should be ignored by `funcfinder.ask`; read more [here](#answers-ignored-when-asking) so that you don't waste your time. 262 | 263 | If the output of your function should be some kind of iterable (e.g. a list or a tuple) and you're not 100% sure what the type will be, consider the functions `assertEqualIters` and `assertDeepEqualIters` from the `funcfinder.utils` module. 264 | 265 | There's one last catch when it comes to asking (and searching for) questions. You probably won't find any answers, because the repo is brand new and contains very few questions and answers. If you find the idea of this repo exciting, if you want it to succeed, it's going to need your help. 266 | 267 | ## Contributing 268 | 269 | It will take a large community effort to make this repo useful. So the first thing you can do to help is recruit others. Tell your friends and coworkers. Talk about funcfinder in programming forums. Write a blog post. Anything that will multiply your impact. 270 | 271 | If you have concerns or suggestions, feel free to open an issue, join the discussion on an existing one, or [come chat on gitter](https://gitter.im/alexmojaki/funcfinder?utm_source=share-link&utm_medium=link&utm_campaign=share-link). All feedback is welcome. 272 | 273 | If you're willing to write some questions and answers, excellent! You can contribute any functions that you think someone else is likely to look for. This might be the case if you: 274 | 275 | * Tried to find it here yourself and couldn't. 276 | * Saw someone else looking for such a function online. 277 | * Have functions in your own code that are generic enough that someone else might want to solve the same problem, especially if *you* are likely to use them again in a different project. 278 | 279 | Be aware that any code contributions fall under the MIT License and anyone else can use the code however they please. 280 | 281 | Writing questions and answers is pretty simple and straightforward, but there are some rules and guidelines that you need to know. 282 | 283 | ### Folder structure 284 | 285 | Questions are placed in modules in the `funcfinder.questions` package. The modules can have any name (other than `__init__`, don't touch those) and can be organised further into nested packages as desired. The aim of this is simply to avoid a single monolithic file of questions. The names of modules have no real semantics, just try to pick a sensible module for each question. The structure of the package `funcfinder.answers` must match `funcfinder.questions` exactly. 286 | 287 | ### Naming 288 | 289 | All questions must be uniquely named, and all answers must be uniquely named, even across packages. A question and an answer can have the same name. `funcfinder show` can be used to easily check if a question name is taken. If there is a naming conflict it should throw an error at runtime, unless the name was defined twice in the same file. 290 | 291 | ### Imports 292 | 293 | All modules must contain the following imports: 294 | 295 | ``` 296 | from __future__ import absolute_import 297 | from funcfinder.utils import * 298 | ``` 299 | 300 | An answers module must also import the corresponding questions module with the alias `q`, i.e. 301 | ```import funcfinder.questions.x.y.z as q``` 302 | 303 | Imports from the standard library should appear at the top as `import module_name`. The forms `import x as y` and `from x import y` are forbidden. 304 | 305 | Imports from within `funcfinder` itself must also be fully qualified, with no alias. You can create an alias inside the answer using assignment, e.g. 306 | ```useful_function = funcfinder.answers.module.useful_function``` 307 | 308 | Other libraries should be imported using the `try_import` function already imported from `funcfinder.utils`, e.g. 309 | 310 | ```sortedcontainers = try_import("sortedcontainers")``` 311 | 312 | The names on the left and right must match. Using this will prevent errors for users who don't have the library installed, but the `ImportError` will still be raised (wrapped in a `TryImportError`) if you try to use the module. This is how the answer `sorted_dict` raised a `TryImportError` in the tests in the example above even though there was no visible import. It also means that IDEs and other tools won't complain about modules that can't be found. 313 | 314 | ### Writing questions 315 | 316 | Questions should: 317 | 318 | * Have at least one answer. If you think leaving unsolved questions should be allowed, share your thoughts [here](https://github.com/alexmojaki/funcfinder/issues/1). 319 | * Include a docstring explaining exactly what the answer function must accomplish. Be as clear as possible and include plenty of detail. Insert synonyms for words so that the question is more likely to be found. Remember that searching for questions is [(for now)](https://github.com/alexmojaki/funcfinder/issues/2) more like using `grep` than Google. 320 | * Use the `assert*` functions from `funcfinder.utils` instead of raw `assert` statements. 321 | * Use `assertEqualIters` and `assertDeepEqualIters` where the flexibility is needed. 322 | * Have little performance overhead relative to their solutions so that timing them shows the difference clearly, although clarity should be preferred over minor optimisations. 323 | * Be thorough, like real unit tests. 324 | * Have a single parameter named `func`. 325 | * Always call `func` with the same number of arguments, and not use keyword arguments. 326 | * Call to a common 'base' question where appropriate instead of repeating tests. 327 | 328 | ### Writing answers 329 | 330 | A solution to a question must be in the module corresponding to the question, i.e. an answer under `funcfinder.answers.x.y.z` must solve a question in `funcfinder.questions.x.y.z`. The exception is if an answer solves questions in multiple modules, but this probably indicates poor question placement. 331 | 332 | All answers must have the `solves` decorator (which has been imported from `funcfinder.utils`), with the solved questions as arguments. For example: 333 | 334 | ``` 335 | @solves(q.sort_dict_by_key, q.always_sorted_dict_by_key) 336 | def sorted_dict(d): 337 | ... 338 | ``` 339 | 340 | An answer doesn't *have* to be marked as solving a question even if it does. For example the answer `sorted_dict` above also solves the question `copy_dict` because `copy_dict` is a requirement of the question `sort_dict_by_key`, but someone who just wants to know how to copy a dictionary doesn't need to know how to sort it. This doesn't mean that there cannot be any 'redundancy' in the `solves` decorator: for example, the question `sort_dict_by_key` is a requirement of the question `always_sorted_dict_by_key` so it might seem that the decorator is stating more than necessary. However by doing this, the answer will show up when someone takes a look at either of the two questions. 341 | 342 | Answers should also: 343 | 344 | * Use `@ask_ignore` when appropriate; [see below](#answers-ignored-when-asking). 345 | * Have a plain signature: no `*args`, `**kwargs`, `arg=default`, or tuple unpacking. 346 | * Use other answers where appropriate. 347 | * Not use libraries whose source isn't easily available and which are neither somewhat popular (e.g. at least 20 stars on GitHub) nor very small. In other words, it should be easy to verify that the library is trustworthy. Good documentation is also important in this regard. 348 | * Never enter an infinite loop unless the input is an infinite iterator. Don't count the iterations of a loop and break when the count is too high, but handle unexpected finite input. For example, the following is unacceptable because it will never terminate if `b` is `-1` or `0.5`: 349 | 350 | ``` 351 | def pow(a, b): 352 | """Returns a^b, where b is a positive integer.""" 353 | result = 1 354 | while b != 0: 355 | result *= a 356 | b -= 1 357 | return result 358 | ``` 359 | 360 | Once you've finished answering a question, run the `funcfinder show` command to make sure it works. If you see that the question has multiple solutions, and one might be significantly faster than another, consider ensuring that the question is able to demonstrate the performance difference. This means adding one or more test cases at the end of the question that have a medium sized input, if none are present. If you do this, remember to run `funcfinder show` again at the end. Don't change the question if one of the answers requires a library that you don't have and aren't willing to install - you don't want to unknowingly break an answer. By the way, the `-t` flag will prevent `funcfinder` from timing answers, just in case that starts to annoy you. 361 | 362 | #### Answers ignored when asking 363 | 364 | There are some kinds of answers that are worth having in the repository for people to find by searching but create problems for users of `funcfinder.ask`. You should use the decorator `@ask_ignore` when you write such a problematic answer. `funcfinder.ask` will then skip over the answer when looking for a solution to a question. 365 | 366 | There are two common cases to use this decorator. The first is if the function is likely to cause a test to take a significant amount of time, even when given 'small' inputs of a common type. Examples include the [Ackermann function](https://en.wikipedia.org/wiki/Ackermann_function) or functions which connect to the Internet. The second case is answers which mutate or modify external resources such as files or databases, or anything else that takes time to set up in a clean state. This includes calling `.close()` or a similar method on anything. This way users can set up these resources for testing outside of a question, speeding up the asking process, and not worry about answers interfering with each other in the test. 367 | 368 | ## FAQ 369 | 370 | **What if I ask a question and my test always throws an exception because I made a mistake?** 371 | If the exception is thrown before any calls to `func` are made, it'll be picked up and shown to you. Otherwise funcfinder will fail to find an answer, just as if there really wasn't one. 372 | 373 | **What if I ask a question looking for a function with multiple arguments?** 374 | No problem. funcfinder will automatically try out every possible rearrangement behind the scenes. It will even rearrange the arguments in the source code it prints for you to match your question. In short, this is not an issue. 375 | -------------------------------------------------------------------------------- /funcfinder/__init__.py: -------------------------------------------------------------------------------- 1 | from bdb import Bdb 2 | import inspect 3 | from itertools import imap, dropwhile, permutations 4 | import pydoc 5 | import re 6 | import sys 7 | import traceback 8 | import timeit 9 | 10 | import wrapt 11 | 12 | import funcfinder.answers 13 | import funcfinder.questions 14 | from utils import TryImportError 15 | 16 | 17 | def search_questions(terms): 18 | if isinstance(terms, basestring): 19 | terms = terms.split() 20 | 21 | terms = map(str.lower, terms) 22 | 23 | print 24 | 25 | found = False 26 | for question in funcfinder.questions.functions.itervalues(): 27 | search_string = (question.__name__ + question.__doc__).lower() 28 | if all(imap(search_string.__contains__, terms)): 29 | found = True 30 | print question.__name__ + ":\n" 31 | print pydoc.getdoc(question) 32 | print "\n-----------------------\n" 33 | 34 | if not found: 35 | print "No questions found" 36 | 37 | 38 | def _show_dependencies(dependencies, existing_sources): 39 | found_dependency = False 40 | if dependencies: 41 | for dependency in dependencies: 42 | dependency_source = _get_source(dependency) 43 | if dependency_source in existing_sources: 44 | continue 45 | 46 | spaceless_dependency_source = re.sub("\s+", "", dependency_source) 47 | found_matching_source = False 48 | for existing_source in existing_sources: 49 | spaceless_existing_source = re.sub("\s+", "", existing_source) 50 | if spaceless_dependency_source in spaceless_existing_source: 51 | found_matching_source = True 52 | break 53 | if found_matching_source: 54 | continue 55 | 56 | if not found_dependency: 57 | print "Dependencies:" 58 | print 59 | found_dependency = True 60 | _show_source(dependency, dependency_source) 61 | 62 | 63 | def show_question(question, time_answers=True): 64 | print 65 | if isinstance(question, basestring): 66 | try: 67 | question = funcfinder.questions.functions[question] 68 | except KeyError: 69 | print "No question with name %s found" % question 70 | return 71 | 72 | sources = set() 73 | dependencies = set() 74 | 75 | _show_source_and_add_to_set(question, sources) 76 | 77 | if hasattr(question, "answers"): 78 | print "Answers:" 79 | print 80 | correct_answers = [] 81 | for answer in question.answers: 82 | dependencies.update(_CodeDetector.detect(question, answer, include_questions=True)) 83 | _show_source_and_add_to_set(answer, sources) 84 | try: 85 | question(answer) 86 | print "Passed tests successfully." 87 | print "--------------------------" 88 | print 89 | correct_answers.append(answer) 90 | except Exception as e: 91 | print "Failed tests with exception:" 92 | if not isinstance(e, TryImportError): 93 | tb_list = traceback.extract_tb(sys.exc_info()[2]) 94 | tb_list = list(dropwhile(lambda entry: entry[2] != question.__name__, tb_list)) 95 | print "".join(traceback.format_list(tb_list)).rstrip() 96 | print "".join(traceback.format_exception_only(*sys.exc_info()[:2])) 97 | 98 | if time_answers: 99 | _time_answers(question, correct_answers) 100 | 101 | _show_dependencies(dependencies, sources) 102 | 103 | else: 104 | print "No answers have been marked as solving this question, which is a problem." 105 | print "The question will now be asked manually. If any solutions are found, please contribute by adding:" 106 | print 107 | print "@solves(q.%s)" % question.__name__ 108 | print 109 | print "to each solution." 110 | print 111 | ask(question, time_answers=time_answers) 112 | 113 | 114 | def _get_source(func, index_permutation=None): 115 | try: 116 | name = func.__name__ 117 | except AttributeError: 118 | name = func.co_name 119 | pattern = r"(def\s+%s\(.+)" % name 120 | regex = re.compile(pattern, re.DOTALL) 121 | source = inspect.getsource(func).strip() 122 | match = regex.search(source) 123 | if match is not None: 124 | source = match.group(1) 125 | if index_permutation and len(index_permutation) > 1 and list(index_permutation) != sorted(index_permutation): 126 | pattern = r"(def\s+%s)\((.+?)\)(.+)" % name 127 | regex = re.compile(pattern, re.DOTALL) 128 | match = regex.search(source) 129 | if match is None: 130 | raise Exception("Failed to extract arguments from function definition:\n" + source) 131 | args = re.split(r"\s*,\s*", match.group(2), flags=re.DOTALL) 132 | args = _permute(args, index_permutation) 133 | args = ", ".join(args) 134 | source = "{start}({args}){rest}".format(start=match.group(1), args=args, rest=match.group(3)) 135 | return source 136 | 137 | 138 | def _show_source(func, source): 139 | print inspect.getsourcefile(func), ":", inspect.getsourcelines(func)[1] 140 | print source 141 | print 142 | 143 | 144 | def _show_source_and_add_to_set(func, sources_set, index_permutation=None): 145 | source = _get_source(func, index_permutation) 146 | sources_set.add(source) 147 | _show_source(func, source) 148 | 149 | 150 | def _time(question, answer, number=1): 151 | def stmt(): 152 | question(answer) 153 | 154 | if number == 1: 155 | time_taken = 0 156 | while time_taken < 1: 157 | number *= 2 158 | time_taken = timeit.timeit(stmt, number=number) 159 | return min(timeit.repeat(stmt, number=number, repeat=5)), number 160 | 161 | 162 | def _time_answers(question, correct_answers): 163 | if len(correct_answers) > 1: 164 | print "Best times per answer:" 165 | number = 1 166 | for answer in correct_answers: 167 | time_taken, number = _time(question, answer, number) 168 | print "%s: %.3f s" % (answer.__name__, time_taken) 169 | print "(among 5 sets of %i repetitions)" % number 170 | print 171 | 172 | 173 | def ask(question, time_answers=True): 174 | num_args_holder = [] 175 | 176 | def count_expected_args(*args): 177 | num_args_holder.append(len(args)) 178 | 179 | exc_info = None 180 | 181 | try: 182 | question(count_expected_args) 183 | except Exception: 184 | exc_info = sys.exc_info() 185 | pass 186 | 187 | if not num_args_holder: 188 | if exc_info: 189 | raise exc_info[0], exc_info[1], exc_info[2] 190 | else: 191 | raise AssertionError("Failed to find the number of arguments the answer must have. " 192 | "Did you call the given function?") 193 | 194 | del exc_info 195 | 196 | num_args = num_args_holder[0] 197 | index_permutations = list(permutations(range(num_args))) 198 | 199 | correct_answers = [] 200 | dependencies = set() 201 | sources = set() 202 | for answer in funcfinder.answers.functions.itervalues(): 203 | if not getattr(answer, "ask_ignore", False) and answer.func_code.co_argcount == num_args: 204 | for index_permutation in index_permutations: 205 | try: 206 | permuted_answer = _permute_args(index_permutation)(answer) 207 | question(permuted_answer) 208 | except (_ForbiddenKwargs, _WrongNumberOfArgs) as e: 209 | print e.message 210 | return 211 | except Exception: 212 | pass 213 | else: 214 | _show_source_and_add_to_set(answer, sources, index_permutation) 215 | solved_questions = getattr(answer, "solved_questions") 216 | if solved_questions: 217 | print "Solves the question%s %s" % ( 218 | "s" * (len(solved_questions) > 1), 219 | ", ".join(q.__name__ for q in solved_questions)) 220 | print 221 | print "-------------------------" 222 | print 223 | correct_answers.append(permuted_answer) 224 | dependencies.update(_CodeDetector.detect(question, permuted_answer, include_questions=False)) 225 | dependencies.discard(answer.func_code) 226 | break 227 | 228 | if not correct_answers: 229 | print "Sorry, no correct answers found. If you find one, please consider contributing it!" 230 | return 231 | 232 | if time_answers: 233 | _time_answers(question, correct_answers) 234 | _show_dependencies(dependencies, sources) 235 | 236 | 237 | def _permute_args(index_permutation): 238 | @wrapt.decorator 239 | def wrapper(wrapped, _, args, kwargs): 240 | if kwargs: 241 | raise _ForbiddenKwargs 242 | if len(index_permutation) != len(args): 243 | raise _WrongNumberOfArgs 244 | return wrapped(*_permute(args, index_permutation)) 245 | 246 | return wrapper 247 | 248 | 249 | def _permute(it, index_permutation): 250 | return (it[i] for i in index_permutation) 251 | 252 | 253 | class _ForbiddenKwargs(Exception): 254 | message = "You cannot ask for a function with keyword arguments." 255 | 256 | 257 | class _WrongNumberOfArgs(Exception): 258 | message = "The function you ask for must always have the same number of arguments." 259 | 260 | 261 | class _CodeDetector(Bdb): 262 | def __init__(self, *args): 263 | Bdb.__init__(self, *args) 264 | self.codes = set() 265 | 266 | def do_clear(self, arg): 267 | pass 268 | 269 | def user_call(self, frame, argument_list): 270 | self.codes.add(frame.f_code) 271 | 272 | @classmethod 273 | def detect(cls, question, answer, include_questions): 274 | detector = cls() 275 | detector.set_trace() 276 | try: 277 | question(answer) 278 | except Exception: 279 | pass 280 | detector.set_quit() 281 | for func in (question, answer): 282 | detector.codes.discard(func.func_code) 283 | 284 | filtered_codes = set() 285 | 286 | for code in detector.codes: 287 | filename = code.co_filename 288 | 289 | def from_package(package): 290 | return ("funcfinder/%s/" % package) in filename and not filename.endswith("__init__.py") 291 | 292 | # noinspection PyTypeChecker 293 | if from_package("answers") or include_questions and from_package("questions"): 294 | filtered_codes.add(code) 295 | return filtered_codes 296 | -------------------------------------------------------------------------------- /funcfinder/__main__.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | from funcfinder import * 4 | 5 | 6 | def find(args): 7 | print "Searching for the terms %s..." % args.terms 8 | search_questions(args.terms) 9 | 10 | 11 | def show(args): 12 | show_question(args.question, time_answers=args.time_answers) 13 | 14 | 15 | def funcfinder_help(): 16 | pass 17 | 18 | 19 | def main(): 20 | parser = ArgumentParser( 21 | description="Find (using docstrings, not tests) and inspect functions in the funcfinder repository.") 22 | subparsers = parser.add_subparsers() 23 | 24 | find_parser = subparsers.add_parser( 25 | "find", 26 | description="Shows questions whose docstrings contain all the given terms, ignoring case.") 27 | find_parser.set_defaults(func=find) 28 | find_parser.add_argument("terms", metavar="TERM", nargs="+") 29 | 30 | show_parser = subparsers.add_parser( 31 | "show", 32 | description="Shows a single question (i.e. test) and the functions that solve it.") 33 | show_parser.set_defaults(func=show) 34 | show_parser.add_argument("question", 35 | help="Name of the question (typically found by first using the find subcommand).") 36 | show_parser.add_argument("-t", "--notime", 37 | help="By default if a question has multiple solutions they are automatically timed, " 38 | "which takes a few seconds. This flag prevents that.", 39 | action="store_false", dest="time_answers") 40 | 41 | args = parser.parse_args() 42 | args.func(args) 43 | 44 | 45 | if __name__ == "__main__": 46 | sys.exit(main()) 47 | -------------------------------------------------------------------------------- /funcfinder/_imports.py: -------------------------------------------------------------------------------- 1 | from pkgutil import walk_packages 2 | from importlib import import_module 3 | import inspect 4 | 5 | 6 | def get_functions(package_path, package_name): 7 | result = {} 8 | for _loader, module_name, ispkg in walk_packages(package_path, prefix=package_name + "."): 9 | module = import_module(module_name) 10 | 11 | def _function_predicate(value): 12 | return (inspect.isfunction(value) and 13 | not value.__name__[0] == "_" and 14 | value.__module__.startswith(module_name)) 15 | 16 | for function_name, function in inspect.getmembers(module, predicate=_function_predicate): 17 | if function_name in result: 18 | raise NameError("The name %s has been defined in both %s and %s." % 19 | (function_name, module_name, result[function_name].__module__)) 20 | result[function_name] = function 21 | return result 22 | -------------------------------------------------------------------------------- /funcfinder/answers/__init__.py: -------------------------------------------------------------------------------- 1 | # noinspection PyProtectedMember 2 | from funcfinder._imports import get_functions 3 | functions = get_functions(__path__, __name__) 4 | -------------------------------------------------------------------------------- /funcfinder/answers/decorator.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import funcfinder.questions.decorator as q 3 | from funcfinder.utils import * 4 | 5 | wrapt = try_import("wrapt") 6 | 7 | 8 | @solves(q.decorator_with_introspection) 9 | def decorator_with_introspection(function, decorator): 10 | # This is not a great use of the wrapt library as you should place the decorator logic inside here. 11 | # However it makes it possible to define a generic function. 12 | @wrapt.decorator 13 | def wrapper(wrapped, _, args, kwargs): 14 | return decorator(wrapped)(*args, **kwargs) 15 | return wrapper(function) 16 | -------------------------------------------------------------------------------- /funcfinder/answers/dict.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import collections 3 | 4 | import funcfinder.questions.dict as q 5 | from funcfinder.utils import * 6 | sortedcontainers = try_import("sortedcontainers") 7 | 8 | 9 | @solves(q.group_by_key_func) 10 | def group_by_key_func(iterable, key_func): 11 | result = collections.defaultdict(list) 12 | for item in iterable: 13 | result[key_func(item)].append(item) 14 | return result 15 | 16 | 17 | @solves(q.sort_dict_by_key) 18 | def ordered_dict_sorted_by_key(d): 19 | return collections.OrderedDict(sorted(d.items())) 20 | 21 | 22 | @solves(q.sort_dict_by_key, q.always_sorted_dict_by_key) 23 | def sorted_dict(d): 24 | return sortedcontainers.SortedDict(d) 25 | 26 | 27 | @solves(q.sort_dict_by_value) 28 | def ordered_dict_sorted_by_value(d): 29 | return collections.OrderedDict(sorted(d.items(), key=lambda item: item[1])) 30 | 31 | 32 | @solves(q.copy_dict) 33 | def copy_dict(d): 34 | return d.copy() 35 | -------------------------------------------------------------------------------- /funcfinder/answers/list.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import itertools 3 | 4 | import funcfinder.questions.list as q 5 | from funcfinder.utils import solves 6 | 7 | 8 | @solves(q.transpose) 9 | def transpose_with_zip(arr): 10 | return zip(*arr) 11 | 12 | 13 | @solves(q.transpose) 14 | def transpose_with_map(arr): 15 | return map(None, *arr) 16 | 17 | 18 | @solves(q.transpose_without_tuples, q.transpose) 19 | def transpose_without_tuples(arr): 20 | return map(list, itertools.izip(*arr)) 21 | 22 | 23 | @solves(q.flatten_2d_list_to_iterable) 24 | def flatten_2d_list_using_generator_comprehension(lists): 25 | return (x for sublist in lists for x in sublist) 26 | 27 | 28 | @solves(q.flatten_2d_list_to_iterable) 29 | def flatten_2d_list_to_iterable_using_chain(lists): 30 | return itertools.chain.from_iterable(lists) 31 | 32 | 33 | @solves(q.flatten_2d_list_to_iterable, q.flatten_2d_list_to_list) 34 | def flatten_2d_list_using_list_comprehension(lists): 35 | return [x for sublist in lists for x in sublist] 36 | 37 | 38 | @solves(q.flatten_2d_list_to_iterable, q.flatten_2d_list_to_list) 39 | def flatten_2d_list_to_list_using_chain(lists): 40 | return list(flatten_2d_list_to_iterable_using_chain(lists)) 41 | 42 | 43 | @solves(q.contains_all) 44 | def contains_all_using_imap(container, contained): 45 | return all(itertools.imap(container.__contains__, contained)) 46 | 47 | 48 | @solves(q.contains_all) 49 | def contains_all_using_generator(container, contained): 50 | return all(x in container for x in contained) 51 | -------------------------------------------------------------------------------- /funcfinder/answers/math.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import funcfinder.questions.math as q 3 | from funcfinder.utils import solves 4 | 5 | 6 | @solves(q.is_divisible_by) 7 | def is_divisible_by(a, b): 8 | return a % b == 0 9 | 10 | 11 | @solves(q.is_even) 12 | def is_even(a): 13 | return is_divisible_by(a, 2) 14 | -------------------------------------------------------------------------------- /funcfinder/answers/string.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import funcfinder.questions.string as q 3 | from funcfinder.utils import solves 4 | 5 | 6 | @solves(q.format_without_nones) 7 | def format_without_nones(format_string, args): 8 | return format_string.format(*("" if arg is None else arg for arg in args)) 9 | 10 | -------------------------------------------------------------------------------- /funcfinder/questions/__init__.py: -------------------------------------------------------------------------------- 1 | # noinspection PyProtectedMember 2 | from funcfinder._imports import get_functions 3 | functions = get_functions(__path__, __name__) 4 | -------------------------------------------------------------------------------- /funcfinder/questions/decorator.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from funcfinder.utils import * 3 | import inspect 4 | 5 | 6 | def decorator_with_introspection(func): 7 | """ 8 | Given a function and a decorator 9 | (a function with a function as an argument that returns a wrapped/wrapper function), 10 | apply the decorator to the function (i.e. return the decorated function) 11 | without affecting the ability to perform introspection on the original function, i.e. check its name, 12 | signature (argument count), source, etc. 13 | """ 14 | def decorator(function): 15 | def double_wrapper(*args): 16 | return function(*args) * 2 17 | return double_wrapper 18 | 19 | def function_with_a_name(a, b, c, d): 20 | # insert comment here 21 | return a + b + c + d 22 | 23 | wrapped = func(function_with_a_name, decorator) 24 | assertEqual(wrapped(1, 2, 3, 4), 20) 25 | assertEqual(wrapped.__name__, "function_with_a_name") 26 | assertEqual(wrapped.func_code.co_argcount, 4) 27 | assertIn("# insert comment here", inspect.getsource(wrapped)) 28 | -------------------------------------------------------------------------------- /funcfinder/questions/dict.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import random 3 | import string 4 | import itertools 5 | 6 | from funcfinder.utils import * 7 | 8 | 9 | def group_by_key_func(func): 10 | """ 11 | Create a dictionary from an iterable such that the keys are the result of evaluating a key function on elements 12 | of the iterable and the values are lists of elements all of which correspond to the key. 13 | """ 14 | assertEqual(func("a bb ccc d ee fff".split(), len), 15 | {1: ["a", "d"], 16 | 2: ["bb", "ee"], 17 | 3: ["ccc", "fff"]}) 18 | 19 | assertEqual(func([-1, 0, 1, 3, 6, 8, 9, 2], lambda x: x % 2), 20 | {0: [0, 6, 8, 2], 21 | 1: [-1, 1, 3, 9]}) 22 | 23 | 24 | def copy_dict(func): 25 | """ 26 | Returns a new separate dict equal to the original. Updates to the copy don't affect the original. 27 | """ 28 | original = {'a': 1, 'd': 2, 'b': 3, 'c': 4} 29 | copy = func(original) 30 | 31 | # An equal but separate copy has been made 32 | assertEqual(original, copy) 33 | assertIsNot(original, copy) 34 | 35 | # Usual key access still works 36 | assertEqual(copy['d'], 2) 37 | 38 | # Deletion works 39 | del copy['d'] 40 | assertIsNone(copy.get('d')) 41 | 42 | # But it doesn't delete the key in the original 43 | assertEqual(original['d'], 2) 44 | 45 | # Insertion works 46 | copy['x'] = 5 47 | assertEqual(copy['x'], 5) 48 | 49 | # And again, doesn't affect the original 50 | assertIsNone(original.get('x')) 51 | 52 | 53 | def sort_dict_by_key(func): 54 | """ 55 | Return a copy of a dict which still supports all the standard operations with the usual API, 56 | plus can be iterated over in sorted order by key. Adding keys to the dict may not necessarily preserve this order - 57 | for that, see always_sorted_dict_by_key. 58 | """ 59 | copy_dict(func) 60 | 61 | # On my machine at least, this dict does not look sorted 62 | original_dict = {'a': 0, 's': 1, 'd': 2, 'f': 3} 63 | sorted_dict = func(original_dict) 64 | 65 | # Iteration is now ordered 66 | assertEqual(sorted_dict.items(), [('a', 0), ('d', 2), ('f', 3), ('s', 1)]) 67 | 68 | # Larger test 69 | sorted_keys = list(itertools.product(string.ascii_lowercase, string.ascii_lowercase)) 70 | shuffled_keys = list(sorted_keys) 71 | for i in xrange(10): 72 | random.shuffle(shuffled_keys) 73 | original_dict = dict(itertools.izip(shuffled_keys, itertools.count())) 74 | sorted_dict = func(original_dict) 75 | assertEqualIters(sorted_keys, sorted_dict.iterkeys()) 76 | 77 | 78 | def always_sorted_dict_by_key(func): 79 | """ 80 | Return a copied dict sorted by key which preserves its order upon updates. 81 | """ 82 | sort_dict_by_key(func) 83 | sorted_dict = func({}) 84 | keys = list(itertools.product(string.ascii_lowercase, string.ascii_lowercase)) 85 | random.shuffle(keys) 86 | for key in keys: 87 | sorted_dict[key] = key 88 | assertEqualIters(sorted(sorted_dict.keys()), sorted_dict.iterkeys()) 89 | 90 | 91 | def sort_dict_by_value(func): 92 | """ 93 | Return a copy of a dict which still supports all the standard operations with the usual API, 94 | plus can be iterated over in sorted order by value. Adding keys to the dict may not necessarily preserve this order. 95 | """ 96 | copy_dict(func) 97 | 98 | # On my machine at least, this dict does not look sorted by value 99 | original_dict = dict(zip(range(4), "dcba")) 100 | sorted_dict = func(original_dict) 101 | 102 | # Iteration is now ordered by value 103 | assertEqual(sorted_dict.items(), [(3, 'a'), (2, 'b'), (1, 'c'), (0, 'd')]) 104 | 105 | # Larger test 106 | sorted_values = list(itertools.product(string.ascii_lowercase, string.ascii_lowercase)) 107 | shuffled_values = list(sorted_values) 108 | for i in xrange(10): 109 | random.shuffle(shuffled_values) 110 | original_dict = dict(itertools.izip(itertools.count(), shuffled_values)) 111 | sorted_dict = func(original_dict) 112 | assertEqualIters(sorted_values, sorted_dict.itervalues()) 113 | -------------------------------------------------------------------------------- /funcfinder/questions/list.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from funcfinder.utils import * 3 | import string 4 | import random 5 | 6 | 7 | def transpose(func): 8 | """ 9 | Swap/exchange/invert the rows and columns in a list of lists/tuples 10 | (nested list, 2D list/array, two dimensional list, table, matrix). 11 | 12 | See also transpose_without_tuples. 13 | """ 14 | assertDeepEqualIters( 15 | func([[1, 2], 16 | [3, 4]]), 17 | [[1, 3], 18 | [2, 4]] 19 | ) 20 | 21 | assertDeepEqualIters( 22 | func([[1, 2, 3], 23 | [4, 5, 6], 24 | [7, 8, 9]]), 25 | [[1, 4, 7], 26 | [2, 5, 8], 27 | [3, 6, 9]] 28 | ) 29 | 30 | # Doesn't have to be a square 31 | assertDeepEqualIters( 32 | func([[1, 2, 3], 33 | [4, 5, 6]]), 34 | [[1, 4], 35 | [2, 5], 36 | [3, 6]] 37 | ) 38 | 39 | # 200 rows, 100 columns 40 | assertDeepEqualIters(func([range(100) for _ in xrange(200)]), 41 | [[i] * 200 for i in xrange(100)]) 42 | 43 | 44 | def transpose_without_tuples(func): 45 | """ 46 | Like transpose, but the result consists of lists again, not tuples, i.e. not 47 | [[1,2], [3,4]] -> [(1,3), (2,4)] 48 | """ 49 | transpose(func) 50 | assertEqual(func([[1, 2], [3, 4]]), 51 | [[1, 3], [2, 4]]) 52 | 53 | 54 | def flatten_2d_list_to_iterable(func): 55 | """ 56 | Flatten (merge) a list of lists (nested list) into a single iterable, not necessarily a concrete list. 57 | Only perform a shallow merge, it need not support 3D lists and beyond, or lists with varying depth, 58 | e.g. [1, 2, [3, 4]]. 59 | Sublists may be different lengths. 60 | 61 | See also flatten_2d_list_to_list. 62 | """ 63 | assertEqualIters(func([[1]]), [1]) 64 | assertEqualIters(func([[1, 2], [3, 4]]), [1, 2, 3, 4]) 65 | assertEqualIters(func([[0], 66 | [1, 2], 67 | [3, 4, 5]]), 68 | range(6)) 69 | assertEqualIters(func([]), []) 70 | 71 | assertEqualIters(func([[i] for i in xrange(10000)]), xrange(10000)) 72 | 73 | before = [] 74 | count = 0 75 | for i in xrange(1, 100): 76 | before.append(range(count, count + i)) 77 | count += i 78 | # before == [[0], [1, 2], [3, 4, 5], ..., [..., count-1]] 79 | assertEqualIters(func(before), xrange(count)) 80 | 81 | 82 | def flatten_2d_list_to_list(func): 83 | """ 84 | Flatten a 2D list into an actual list, not just any iterable. 85 | """ 86 | flatten_2d_list_to_iterable(func) 87 | assertEqual(func([[1, 2], [3, 4]]), [1, 2, 3, 4]) 88 | 89 | 90 | def contains_all(func): 91 | """ 92 | Return whether the first argument (string, list, tuple, set, or anything else with a __contains__ method) 93 | contains all the elements in the second (any iterable). 94 | """ 95 | assertTrue(func([1, 2], [1])) 96 | assertTrue(func([1, 2], [2, 1])) 97 | assertFalse(func([1, 2], [3])) 98 | assertFalse(func([1, 2], [2, 3])) 99 | assertTrue(func([1, 2, 3], [2, 3])) 100 | 101 | assertTrue(func("the quick brown fox jumps over the lazy dog", string.ascii_lowercase)) 102 | assertFalse(func("the quick brown fox jumps over the dog", string.ascii_lowercase)) 103 | 104 | assertTrue(func({1, 2, 3}, (1, 3))) 105 | assertFalse(func((1, 3), {2, 3})) 106 | 107 | source = range(200) 108 | random.shuffle(source) 109 | container = [] 110 | contained = [] 111 | while source: 112 | contained.append(source.pop()) 113 | assertFalse(func(container, contained)) 114 | container.append(contained[-1]) 115 | assertTrue(func(container, contained)) 116 | container.append(source.pop()) 117 | assertTrue(func(container, contained)) 118 | 119 | container = list(string.digits + string.letters + string.punctuation) 120 | random.shuffle(container) 121 | container = ''.join(container) 122 | for length in xrange(5, 10): 123 | assertTrue(func(container, 124 | (container[i:i+length] for i in xrange(0, len(container) - length, length + 2)))) 125 | -------------------------------------------------------------------------------- /funcfinder/questions/math.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from funcfinder.utils import * 3 | 4 | 5 | def is_divisible_by(func): 6 | """ 7 | Returns True if the first number is divisible by the second, otherwise False. 8 | """ 9 | assertTrue(func(4, 2)) 10 | assertTrue(func(6, 2)) 11 | assertTrue(func(6, 3)) 12 | assertTrue(func(9, 3)) 13 | assertFalse(func(4, 3)) 14 | assertFalse(func(9, 2)) 15 | 16 | assertTrue(all(func(x, 1) for x in xrange(20))) 17 | assertTrue(all(func(120, x) for x in xrange(1, 7))) 18 | assertFalse(func(120, 7)) 19 | 20 | 21 | def is_even(func): 22 | """ 23 | Returns True if the number is even, otherwise False. 24 | """ 25 | assertTrue(func(2)) 26 | assertFalse(func(3)) 27 | assertTrue(func(4)) 28 | 29 | assertTrue(func(2.0)) 30 | assertFalse(func(3.0)) 31 | assertTrue(func(4.0)) 32 | 33 | even = True 34 | for i in xrange(-100, 100): 35 | assertEqual(func(i), even) 36 | even = not even 37 | -------------------------------------------------------------------------------- /funcfinder/questions/string.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from funcfinder.utils import * 3 | 4 | 5 | def format_without_nones(func): 6 | """ 7 | Given a format string in str.format style and a list of arguments, format the string but output an empty string 8 | for arguments that are None instead of the string 'None'. 9 | """ 10 | assertEqual(func("{}a{}b{}c{}", [1, None, 2, None]), "1ab2c") 11 | -------------------------------------------------------------------------------- /funcfinder/utils.py: -------------------------------------------------------------------------------- 1 | from itertools import izip_longest 2 | import importlib 3 | 4 | 5 | def solves(*questions): 6 | """ 7 | Indication that this function satisfies the tests in `questions`. 8 | """ 9 | 10 | def real_decorator(answer): 11 | answer.solved_questions = questions 12 | for question in questions: 13 | question.answers = getattr(question, "answers", []) + [answer] 14 | return answer 15 | 16 | return real_decorator 17 | 18 | 19 | def ask_ignore(answer): 20 | answer.ask_ignore = True 21 | return answer 22 | 23 | 24 | def try_import(module_name): 25 | try: 26 | return importlib.import_module(module_name) 27 | except ImportError as e: 28 | return _ModulePlaceholder(TryImportError(e)) 29 | 30 | 31 | class _ModulePlaceholder(object): 32 | 33 | def __init__(self, exc): 34 | self.exc = exc 35 | 36 | def __getattribute__(self, item): 37 | raise object.__getattribute__(self, "exc") 38 | 39 | 40 | class TryImportError(ImportError): 41 | pass 42 | 43 | 44 | def assertTrue(expr): 45 | assert expr 46 | 47 | 48 | def assertFalse(expr): 49 | assert not expr 50 | 51 | 52 | def assertEqual(a, b): 53 | assert a == b 54 | 55 | 56 | def assertNotEqual(a, b): 57 | assert a != b 58 | 59 | 60 | def assertIs(a, b): 61 | assert a is b 62 | 63 | 64 | def assertIsNot(a, b): 65 | assert a is not b 66 | 67 | 68 | def assertIsNone(a): 69 | assert a is None 70 | 71 | 72 | def assertIsNotNone(a): 73 | assert a is not None 74 | 75 | 76 | def assertIn(a, b): 77 | assert a in b 78 | 79 | 80 | def assertNotIn(a, b): 81 | assert a not in b 82 | 83 | 84 | def assertIsInstance(a, b): 85 | assert isinstance(a, b) 86 | 87 | 88 | def assertIsNotInstance(a, b): 89 | assert not isinstance(a, b) 90 | 91 | 92 | def assertAlmostEqual(a, b): 93 | assert round(a - b, 7) == 0 94 | 95 | 96 | def assertNotAlmostEqual(a, b): 97 | assert round(a - b, 7) != 0 98 | 99 | 100 | def assertEqualIters(a, b): 101 | """ 102 | Assert that two iterables have equal elements in order, whether they are lists, tuples, strings, iterators, etc. 103 | For example 104 | assertEqual([1, 2, 3], (1, 2, 3)) 105 | will fail but 106 | assertEqualIters([1, 2, 3], (1, 2, 3)) 107 | will pass. 108 | This is a shallow comparison: for nested iterables use assertDeepEqualIters. 109 | """ 110 | assert all(i1 == i2 for i1, i2 in izip_longest(a, b, fillvalue=object())) 111 | 112 | 113 | def assertDeepEqualIters(a, b): 114 | """ 115 | Assert that nested iterables have equal elements in order, regardless of iterable type 116 | like assertEqualIters, but arbitrarily deep and regardless of structure. 117 | For example, 118 | ["ab", "cd", "ef", "gh", 1, [[[2]]]] 119 | is not equal to 120 | ["ab", ["c", "d"], ("e", "f"), (c for c in "gh"), 1, [[[2]]]] 121 | not even according to assertEqualIters, but according to this function they are. 122 | """ 123 | try: 124 | iter(a) 125 | except TypeError: 126 | assert a == b 127 | else: 128 | 129 | # Avoid infinite recursion for single character strings 130 | if isinstance(a, basestring) and len(a) == 1: 131 | assert a == b 132 | return 133 | 134 | for i1, i2 in izip_longest(a, b, fillvalue=object()): 135 | assertDeepEqualIters(i1, i2) 136 | 137 | 138 | def assertRaises(callableObj=None, *args): 139 | """ 140 | Fail unless an exception is raised by callableObj when invoked 141 | with arguments args. 142 | 143 | If called without arguments, will return a context object used like this:: 144 | 145 | with assertRaises(): 146 | do_something() 147 | """ 148 | context = _AssertRaisesContext() 149 | if callableObj is None: 150 | return context 151 | with context: 152 | callableObj(*args) 153 | 154 | 155 | class _AssertRaisesContext(object): 156 | """A context manager used to implement assertRaises""" 157 | 158 | def __enter__(self): 159 | return self 160 | 161 | # noinspection PyUnusedLocal 162 | def __exit__(self, exc_type, _exc_value, _tb): 163 | assert exc_type is not None 164 | return True 165 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='funcfinder', 4 | version='0.1', 5 | description='A tool for automatically solving problems of the form "I need a python function that does X."', 6 | url='https://github.com/alexmojaki/funcfinder', 7 | author='Alex Hall', 8 | author_email='alex.mojaki@gmail.com', 9 | license='MIT', 10 | packages=['funcfinder'], 11 | zip_safe=False, 12 | entry_points={ 13 | 'console_scripts': ['funcfinder=funcfinder.__main__:main'], 14 | }, 15 | requires=['wrapt']) 16 | --------------------------------------------------------------------------------