├── .flake8 ├── .github └── workflows │ ├── publish-to-test-pypi.yml │ └── python-app.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── emojisearcher ├── __init__.py ├── preferences.py └── script.py ├── mypy.ini ├── pyproject.toml ├── pytest.ini ├── requirements.in ├── requirements.txt └── tests ├── __init__.py └── test_script.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501 3 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build-n-publish: 7 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python "3.11" 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: "3.11" 15 | - name: Install pypa/build 16 | run: >- 17 | python -m 18 | pip install 19 | build 20 | --user 21 | - name: Build a binary wheel and a source tarball 22 | run: >- 23 | python -m 24 | build 25 | --sdist 26 | --wheel 27 | --outdir dist/ 28 | . 29 | - name: Publish distribution 📦 to Test PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 33 | repository_url: https://test.pypi.org/legacy/ 34 | skip_existing: true 35 | - name: Publish distribution 📦 to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Run flake8 + pytest-cov 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python "3.11" 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: "3.11" 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install -r requirements.txt 19 | - name: Lint with flake8 20 | run: | 21 | # stop the build if there are Python syntax errors or undefined names 22 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 23 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 24 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 25 | - name: Test with pytest with coverage 26 | run: | 27 | pytest --cov=emojisearcher --cov-report=term-missing --cov-fail-under=80 28 | env: 29 | EMOJI_PREFERENCES: ".preferences-test" 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .preferences 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022+ Pybites 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup 2 | setup: 3 | python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt 4 | 5 | .PHONY: lint 6 | lint: 7 | flake8 emojisearcher tests 8 | 9 | .PHONY: typing 10 | typing: 11 | mypy emojisearcher tests 12 | 13 | .PHONY: cov 14 | cov: 15 | pytest --cov=emojisearcher --cov-report=term-missing --cov-fail-under=80 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Pybites Emoji Searcher 2 | 3 | I have been googling emojis and manually copying them to my clipboard. 4 | 5 | Except for Slack + GitHub, there the `:` + autocomplete works great. For other tools, for example Facebook or plain blog / email writing, I needed a better way. 6 | 7 | So here is a tool to look up emojis by text from the command line and automatically copy matching ones to the clipboard (using the awesome [pyperclip](https://pyperclip.readthedocs.io/en/latest/) tool). 8 | 9 | By default it takes the first match in case there are multiple matching emojis. However if you append a dot (.) to a word you get to choose which emoji gets copied. You can also use a `.preferences` file to store overriding emojis or ones this tool does not provide. 10 | 11 | I hope you enjoy this tool and don't hesitate to reach out to me by email: bob@pybit.es or just open an issue / open a PR if you see any opportunity for improvements. 12 | 13 | ### How to install and run it 14 | 15 | ``` 16 | $ git clone git@github.com:bbelderbos/emojisearcher.git 17 | $ cd emojisearcher 18 | $ python3.10 -m venv venv 19 | $ source venv/bin/activate 20 | (venv) $ pip install -r requirements.txt 21 | 22 | # or in one command 23 | $ make setup 24 | 25 | # search from cli 26 | (venv) $ python -m emojisearcher.script bicep 27 | Copied 💪 to clipboard 28 | 29 | (venv) $ python -m emojisearcher.script snake 30 | Copied 🐍 to clipboard 31 | 32 | (venv) $ python -m emojisearcher.script tada 33 | Copied 🎉 to clipboard 34 | 35 | # search interactively (specially useful if there are multiple matches, you can choose) 36 | 37 | (venv) $ python -m emojisearcher.script 38 | 39 | 40 | ------------------------------------------------------------------------------------ 41 | Type one or more emoji related words ... 42 | End a word with a . if you want to select an emoji if there are multiple 43 | matches, otherwise the first match will be picked. Type 'q' to exit. 44 | > snake 45 | Copied 🐍 to clipboard 46 | 47 | ------------------------------------------------------------------------------------ 48 | Type one or more emoji related words ... 49 | End a word with a . if you want to select an emoji if there are multiple 50 | matches, otherwise the first match will be picked. Type 'q' to exit. 51 | > grin 52 | Copied 😺 to clipboard 53 | 54 | ------------------------------------------------------------------------------------ 55 | Type one or more emoji related words ... 56 | End a word with a . if you want to select an emoji if there are multiple 57 | matches, otherwise the first match will be picked. Type 'q' to exit. 58 | > grin. 59 | 1 😺 60 | 2 😸 61 | 3 😀 62 | 4 😃 63 | 5 😄 64 | 6 😅 65 | 7 😆 66 | 8 😀 67 | 9 😁 68 | Select the number of the emoji you want: 4 69 | Copied 😃 to clipboard 70 | 71 | ------------------------------------------------------------------------------------ 72 | Type one or more emoji related words ... 73 | End a word with a . if you want to select an emoji if there are multiple 74 | matches, otherwise the first match will be picked. Type 'q' to exit. 75 | > q 76 | Bye 77 | ``` 78 | 79 | ### Ease of use: make a shell alias 80 | 81 | Using a shell alias can be really convenient for this (assuming you have the project cloned in `~/code`): 82 | 83 | ``` 84 | # .zshrc 85 | function emo { 86 | # subshell so you don't stay in the virtual env after running it 87 | (cd $HOME/code/emojisearcher && source venv/bin/activate && python -m emojisearcher.script "$@") 88 | } 89 | 90 | $ source ~/.zshrc 91 | $ emo snake 92 | Copied 🐍 to clipboard 93 | 94 | # or get multiple emojis at once 95 | $ emo snake bicep tada heart fire 96 | Copied 🐍 💪 🎉 💓 🔥 to clipboard 97 | ``` 98 | 99 | After sourcing your .zshrc you can now get emojis copied to your clipboard fast using `emo bicep`, `emo tada` etc. 100 | 101 | ### Preferred emojis 102 | 103 | _This section uses the shell alias I created in the previous step._ 104 | 105 | Sometimes you don't get a match: 106 | 107 | ``` 108 | $ emo ninja 109 | No matches for ninja 110 | ``` 111 | 112 | Or you get way too many: 113 | 114 | ``` 115 | $ emo heart. 116 | 1 💓 117 | 2 🖤 118 | ... 119 | ... 120 | 35 😻 121 | 36 😍 122 | Select the number of the emoji you want: 36 123 | Copied 😍 to clipboard 124 | ``` 125 | 126 | And some don't work (not sure why yet ...): 127 | 128 | ``` 129 | $ emo question 130 | Copied to clipboard 131 | ``` 132 | 133 | Since 0.6.0 you can create a `.preferences` file to create a mapping of missing / preferred emojis which will take precedence. 134 | 135 | You can create this file in the root folder of the project or use the `EMOJI_PREFERENCES` environment variable to store it somewhere else: 136 | 137 | ``` 138 | $ export EMOJI_PREFERENCES=/Users/bbelderbos/.emoji_preferences 139 | ``` 140 | 141 | Let's look at this in action. Normally the tool would work like this: 142 | 143 | ``` 144 | $ emo heart 145 | Copied 💓 to clipboard 146 | $ emo cool 147 | Copied 🆒 to clipboard 148 | ``` 149 | 150 | Say you added a preferences file like this: 151 | 152 | ``` 153 | $ cat .preferences 154 | ninja:🥷 # missing (and much needed) 155 | # overrides 156 | eyes:😍 # replaces default 😁 157 | heart:❤️ # replaces default 💓 158 | hearts:💕 # replaces default 💞 159 | # easier to remember 160 | idea:💡 # also matches "bulb" 161 | # trying to fix non-matching emojis 162 | bliksem:⚡️ # this is Dutch 163 | faster:🏃 164 | ``` 165 | 166 | Note that you can use (inline) comments. 167 | 168 | Now with the preferences in place your shiny new emojis kick in first 🎉 169 | 170 | ``` 171 | $ emo heart 172 | Copied ❤️ to clipboard 173 | 174 | (no more 💓) 175 | 176 | $ emo cool 177 | Copied 😎 to clipboard 178 | 179 | (no more 🆒) 180 | ``` 181 | 182 | Enjoy! 183 | 184 | ### Running the tests and other tools 185 | 186 | ``` 187 | (venv) $ pytest 188 | # or 189 | (venv) $ make cov 190 | 191 | # run flake8 and mypy 192 | (venv) $ make lint 193 | (venv) $ make typing 194 | ``` 195 | 196 | ### Rich 197 | 198 | Originally Around 0.0.5 we started using `rich` to retrieve a list of emojis, it seems a bit more accurate (e.g. our beloved tada 🎉 emoji was missing!) 199 | 200 | ### OS alternatives 201 | 202 | While sharing this [On Twitter](https://twitter.com/bbelderbos/status/1374414940988043264) I learned about other ways to get emojis (thanks Matt Harrison): 203 | 204 | - Windows: Windows logo key + . (period) 205 | 206 | - Mac: CTRL + CMD + Space 207 | 208 | Trying this on Mac, this does require the mouse though and it does not copy the emoji to your clipboard. 209 | -------------------------------------------------------------------------------- /emojisearcher/__init__.py: -------------------------------------------------------------------------------- 1 | """Look up emojis by text and copy them to the clipboard.""" 2 | 3 | __version__ = "0.6.0" 4 | -------------------------------------------------------------------------------- /emojisearcher/preferences.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import re 4 | 5 | MATCH_PREF_RE = re.compile(r"(\S+):(\S).*$") # discard everything after emoji 6 | DEFAULT_PREFERENCES_FILE = ".preferences" 7 | 8 | 9 | def _is_comment(line: str) -> bool: 10 | return line.startswith("#") 11 | 12 | 13 | def _load_preferences_file() -> str | None: 14 | try: 15 | prefs_file = os.environ.get("EMOJI_PREFERENCES", DEFAULT_PREFERENCES_FILE) 16 | return Path(prefs_file).read_text() 17 | except FileNotFoundError: 18 | return None 19 | 20 | 21 | def load_preferences() -> dict[str, str]: 22 | preferences: dict[str, str] = {} 23 | content = _load_preferences_file() 24 | if content is None: 25 | return preferences 26 | 27 | for line in content.splitlines(): 28 | if _is_comment(line): 29 | continue 30 | 31 | # could do a dictcomp but want to graciously ignore non matches 32 | match_ = MATCH_PREF_RE.match(line) 33 | if match_: 34 | description, emoji = match_.groups() 35 | # not lowercasing the description here, that is making 36 | # preferences case sensitive so user can have different 37 | # emojis for Ninja, NINJA and ninja 38 | preferences[description] = emoji 39 | 40 | return preferences 41 | -------------------------------------------------------------------------------- /emojisearcher/script.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | from rich._emoji_codes import EMOJI as EMOJI_MAPPING 5 | from pyperclip import copy 6 | 7 | from .preferences import load_preferences 8 | 9 | QUIT = 'q' 10 | SIGNAL_CHAR = '.' 11 | PROMPT = f""" 12 | ------------------------------------------------------------------------------------ 13 | Type one or more emoji related words ... 14 | End a word with a {SIGNAL_CHAR} if you want to select an emoji if there are multiple 15 | matches, otherwise the first match will be picked. Type 'q' to exit. 16 | > """ 17 | NON_EMOJI_CHARS = re.compile('[^\U00010000-\U0010ffff]', 18 | flags=re.UNICODE) 19 | 20 | 21 | def clean_non_emoji_characters(emoji: str) -> str: 22 | return NON_EMOJI_CHARS.sub(r'', emoji) 23 | 24 | 25 | def get_matching_emojis( 26 | words: list[str], 27 | *, 28 | preferences: dict[str, str] | None = None, 29 | interactive: bool = False 30 | ) -> list[str]: 31 | """ 32 | Traverse words list finding matching emojis. 33 | 34 | If a preference emoji is set that takes precedence. 35 | 36 | If there are multiple matches take the first one unless 37 | interactive is set to True or the word ends with a SIGNAL_CHAR, 38 | which means user specified desire for interactive lookup. 39 | 40 | Making preferences "injectable" makes it testable and the 41 | existence of a .preferences does not mess with the tests. 42 | """ 43 | if preferences is None: 44 | preferences = load_preferences() 45 | 46 | matches = [] 47 | is_preference_emoji = False 48 | for word in words: 49 | if word in preferences: 50 | emojis = [preferences[word]] 51 | is_preference_emoji = True 52 | else: 53 | emojis = get_emojis_for_word(word.rstrip(SIGNAL_CHAR)) 54 | if len(emojis) == 0: 55 | continue 56 | 57 | interactive_mode = word.endswith(SIGNAL_CHAR) or interactive 58 | if len(emojis) > 1 and interactive_mode: 59 | selected_emoji = user_select_emoji(emojis) 60 | if selected_emoji is None: 61 | continue 62 | else: 63 | selected_emoji = emojis[0] 64 | 65 | matches.append( 66 | selected_emoji if is_preference_emoji else 67 | clean_non_emoji_characters(selected_emoji) 68 | ) 69 | 70 | return matches 71 | 72 | 73 | def get_emojis_for_word( 74 | word: str, emoji_mapping: dict[str, str] = EMOJI_MAPPING 75 | ) -> list[str]: 76 | return [emo for name, emo in emoji_mapping.items() if word in name] 77 | 78 | 79 | def user_select_emoji(emojis: list[str]) -> str | None: 80 | while True: 81 | try: 82 | for i, emo in enumerate(emojis, start=1): 83 | print(i, emo) 84 | user_input = input("Select the number of the emoji you want: ") 85 | idx = int(user_input) 86 | return emojis[idx - 1] 87 | except ValueError: 88 | print(f"{user_input} is not an integer.") 89 | continue 90 | except IndexError: 91 | print(f"{user_input} is not a valid option.") 92 | continue 93 | except KeyboardInterrupt: 94 | print(" Exiting selection menu.\n") 95 | return None 96 | 97 | 98 | def copy_emojis_to_clipboard(matches: list[str]) -> None: 99 | all_matching_emojis = ' '.join(matches) 100 | print(f"Copied {all_matching_emojis} to clipboard") 101 | copy(all_matching_emojis) 102 | 103 | 104 | def _match_emojis(text): 105 | words = text.split() 106 | matches = get_matching_emojis(words) 107 | if matches: 108 | copy_emojis_to_clipboard(matches) 109 | else: 110 | print(f"No matches for {text}") 111 | 112 | 113 | def main(args): # pragma: no cover 114 | if not args: 115 | while True: 116 | user_input = input(PROMPT) 117 | user_input = user_input.lower() 118 | if user_input == QUIT: 119 | print('Bye') 120 | break 121 | 122 | _match_emojis(user_input) 123 | else: 124 | text = " ".join(args) 125 | _match_emojis(text) 126 | 127 | 128 | if __name__ == "__main__": # pragma: no cover 129 | main(sys.argv[1:]) 130 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "emojisearcher" 7 | authors = [{name = "Bob Belderbos", email = "bob@pybit.es"}] 8 | readme = "README.md" 9 | requires-python = ">=3.10" 10 | classifiers = ["License :: OSI Approved :: MIT License"] 11 | dynamic = ["version", "description"] 12 | dependencies = [ 13 | "pyperclip >=1.8.2", 14 | "rich >=12.3.0", 15 | ] 16 | 17 | [tool.flit.module] 18 | name = "emojisearcher" 19 | 20 | [project.optional-dependencies] 21 | test = [ 22 | "pytest", 23 | ] 24 | 25 | [project.urls] 26 | Source = "https://github.com/bbelderbos/emojisearcher" 27 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env = 3 | EMOJI_PREFERENCES=.preferences-test 4 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | pyperclip 2 | rich 3 | # dev 4 | pytest 5 | pytest-cov 6 | pytest-env 7 | mypy 8 | flake8 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.10 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | attrs==22.1.0 8 | # via pytest 9 | commonmark==0.9.1 10 | # via rich 11 | coverage[toml]==6.5.0 12 | # via pytest-cov 13 | exceptiongroup==1.0.4 14 | # via pytest 15 | flake8==6.0.0 16 | # via -r requirements.in 17 | iniconfig==1.1.1 18 | # via pytest 19 | mccabe==0.7.0 20 | # via flake8 21 | mypy==0.991 22 | # via -r requirements.in 23 | mypy-extensions==0.4.3 24 | # via mypy 25 | packaging==21.3 26 | # via pytest 27 | pluggy==1.0.0 28 | # via pytest 29 | pycodestyle==2.10.0 30 | # via flake8 31 | pyflakes==3.0.1 32 | # via flake8 33 | pygments==2.13.0 34 | # via rich 35 | pyparsing==3.0.9 36 | # via packaging 37 | pyperclip==1.8.2 38 | # via -r requirements.in 39 | pytest==7.2.0 40 | # via 41 | # -r requirements.in 42 | # pytest-cov 43 | # pytest-env 44 | pytest-cov==4.0.0 45 | # via -r requirements.in 46 | pytest-env==0.8.1 47 | # via -r requirements.in 48 | rich==12.3.0 49 | # via -r requirements.in 50 | tomli==2.0.1 51 | # via 52 | # coverage 53 | # mypy 54 | # pytest 55 | typing-extensions==4.4.0 56 | # via mypy 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbelderbos/emojisearcher/f0a4f1d32d847674362363b8669ba5b9a9874f16/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_script.py: -------------------------------------------------------------------------------- 1 | from inspect import cleandoc 2 | import os 3 | from pathlib import Path 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from emojisearcher.preferences import load_preferences 9 | from emojisearcher.script import (clean_non_emoji_characters, 10 | get_matching_emojis, 11 | get_emojis_for_word, 12 | user_select_emoji) 13 | 14 | PREFERENCE_FILE_CONTENT = """ 15 | ninja:🥷 # missing (and much needed) 16 | # overrides 17 | eyes:😍 # replaces default 😁 18 | heart:❤️ # replaces default 💓 19 | hearts:💕 # replaces default 💞 20 | # easier to remember 21 | idea:💡 # also matches "bulb" 22 | # trying to fix non-working emojis 23 | bliksem:⚡️ # this is Dutch 24 | faster:🏃 25 | """ 26 | 27 | 28 | @pytest.fixture(scope="session") 29 | def add_preferences(): 30 | prefs_file = os.environ.get("EMOJI_PREFERENCES") 31 | assert prefs_file is not None 32 | 33 | prefs_file = Path(prefs_file) 34 | prefs_file.write_text(PREFERENCE_FILE_CONTENT) 35 | 36 | yield 37 | 38 | prefs_file.unlink() 39 | 40 | 41 | @pytest.mark.parametrize("word, expected", [ 42 | ("🤽\u200d♂️'", "🤽"), 43 | ("12🤽34", "🤽"), 44 | ("abc🤽ñ=)", "🤽"), 45 | ]) 46 | def test_clean_non_emoji_characters(word, expected): 47 | assert clean_non_emoji_characters(word) == expected 48 | 49 | 50 | @pytest.mark.parametrize("words, matches", [ 51 | ("heart snake beer", ['💓', '🐍', '🍺']), 52 | ("hand scream angry", ['👌', '😱', '😠']), 53 | ("struck dog", ['🤩', '🐶']), 54 | ("slee tree fire water cat", ['😴', '🎄', '🔥', '🤽', '🈸']), 55 | ("ninja", []), # following do work with *with_preferences :) 56 | ("eyes", ["😁"]), 57 | ("bliksem", []), # this is Dutch, we use it as preference below 58 | ("faster", []), 59 | ]) 60 | def test_get_matching_emojis(words, matches): 61 | assert get_matching_emojis(words.split(), preferences={}) == matches 62 | 63 | 64 | @pytest.mark.parametrize("words, matches", [ 65 | ("ninja", ['🥷']), 66 | ("eyes", ['😍']), 67 | ("hearts idea", ['💕', '💡']), # prefs work with 2 as well 68 | ("faster", ['🏃']), 69 | ]) 70 | def test_user_preferences(add_preferences, words, matches): 71 | preferences = load_preferences() 72 | assert get_matching_emojis(words.split(), preferences=preferences) == matches 73 | 74 | 75 | @patch("emojisearcher.script.user_select_emoji", side_effect=['💓']) 76 | def test_get_emojis_for_word_with_user_input(mock_user_inp): 77 | matches = get_matching_emojis(["heart"], preferences={}, interactive=True) 78 | assert matches[0] == '💓' 79 | 80 | 81 | @patch("emojisearcher.script.user_select_emoji", side_effect=[None]) 82 | def test_get_emojis_for_word_with_user_cancelling(mock_user_inp): 83 | assert get_matching_emojis(["heart"], preferences={}, interactive=True) == [] 84 | 85 | 86 | def test_user_prefs_with_larger_emoji(add_preferences): 87 | preferences = load_preferences() 88 | matches = get_matching_emojis(["bliksem"], preferences=preferences) 89 | encoded_actual_emoji = matches[0].encode('unicode-escape') 90 | assert len(encoded_actual_emoji) == 6 91 | encoded_expected_emoji = '⚡️'.encode('unicode-escape') 92 | assert len(encoded_expected_emoji) == 12 93 | assert encoded_actual_emoji in encoded_actual_emoji 94 | 95 | 96 | @pytest.mark.parametrize("word, num_results, emoji", [ 97 | ("heart", 36, '💓'), 98 | ("snake", 1, '🐍'), 99 | ("grin", 9, '😺'), 100 | ]) 101 | def test_get_emojis_for_word(word, num_results, emoji): 102 | result = get_emojis_for_word(word) 103 | assert len(result) == num_results 104 | assert result[0] == emoji 105 | 106 | 107 | @patch("builtins.input", side_effect=['a', 10, 2, 'q']) 108 | def test_user_selects_tree_emoji(mock_input, capfd): 109 | trees = ['🎄', '🌳', '🌲', '🌴', '🎋'] 110 | ret = user_select_emoji(trees) 111 | assert ret == "🌳" 112 | actual = capfd.readouterr()[0].strip() 113 | expected = cleandoc(""" 114 | 1 🎄 115 | 2 🌳 116 | 3 🌲 117 | 4 🌴 118 | 5 🎋 119 | a is not an integer. 120 | 1 🎄 121 | 2 🌳 122 | 3 🌲 123 | 4 🌴 124 | 5 🎋 125 | 10 is not a valid option. 126 | 1 🎄 127 | 2 🌳 128 | 3 🌲 129 | 4 🌴 130 | 5 🎋 131 | """) 132 | assert actual == expected 133 | 134 | 135 | def test_load_empty_file(): 136 | with pytest.MonkeyPatch.context() as mp: 137 | mp.setenv('EMOJI_PREFERENCES', 'non-existent-prefs-file') 138 | assert load_preferences() == {} 139 | --------------------------------------------------------------------------------