diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..2a818e0
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,30 @@
+name: Tests
+on: [push, pull_request]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+ - name: Install and set up Poetry
+ run: |
+ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
+ source $HOME/.poetry/env
+ poetry config virtualenvs.in-project true
+ - name: Install dependencies
+ run: |
+ source $HOME/.poetry/env
+ poetry install
+ - name: Test
+ run: |
+ source $HOME/.poetry/env
+ poetry run tox -q --parallel=auto
+ - name: Post code coverage results
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+ run: |
+ pip install 'coveralls<2'
+ coveralls
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d82fa7a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,143 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# static files generated from Django application using `collectstatic`
+media
+static
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..00c269b
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,10 @@
+Changelog
+=========
+
+All notable changes to this project will be documented here. This changelog
+follows the conventions of `keepachangelog.com `_.
+
+Contributors
+~~~~~~~~~~~~
+
+- Tucker Siemens (`@reillysiemens `_)
diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst
new file mode 100644
index 0000000..48f5514
--- /dev/null
+++ b/CODE_OF_CONDUCT.rst
@@ -0,0 +1,140 @@
+Contributor Covenant Code of Conduct
+====================================
+
+Our Pledge
+----------
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+Our Standards
+-------------
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+Enforcement Responsibilities
+----------------------------
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for
+moderation decisions when appropriate.
+
+Scope
+-----
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+Enforcement
+-----------
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement via email. All
+complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+Enforcement Guidelines
+----------------------
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+1. Correction
+~~~~~~~~~~~~~
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+2. Warning
+~~~~~~~~~~
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+3. Temporary Ban
+~~~~~~~~~~~~~~~~
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+4. Permanent Ban
+~~~~~~~~~~~~~~~~
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+Attribution
+-----------
+
+This Code of Conduct is adapted from the `Contributor Covenant`_, version 2.0,
+available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by
+`Mozilla's code of conduct enforcement ladder`_.
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
+
+.. _Contributor Covenant: https://www.contributor-covenant.org
+.. _Mozilla's code of conduct enforcement ladder: https://github.com/mozilla/diversity
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..69c5441
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,151 @@
+Contributing Guidelines
+=======================
+
+Please follow these guidelines for contributing to this project.
+
+Repository Management
+---------------------
+
+- Fork this project into your own repository.
+- Follow the `version control`_ guidelines.
+- No changes should reach the ``master`` branch except by way of a
+ `pull request`_.
+
+Submitting Issues
+~~~~~~~~~~~~~~~~~
+
+`Create an issue`_ to report bugs or request enhancements. Better yet, you're
+encouraged to fix the problem yourself and submit a `pull request`_.
+
+Bug Reports
++++++++++++
+
+When reporting a bug, please provide the steps to reproduce the problem and any
+details that could be important such as whether this is the first time this has
+happened or whether others are experiencing it.
+
+Pull Requests
++++++++++++++
+
+Pull requests must remain focused on fixing or addressing one thing (see
+`topic branch`_ model). Make sure your pull request contains a clear title and
+description. Test coverage should not drop as a result. If you add code, you
+add tests.
+
+Be sure to follow the guidelines on `writing code`_ if you want your work
+considered for inclusion.
+
+Handling Pull Requests
+~~~~~~~~~~~~~~~~~~~~~~
+
+- Pull requests **must** include:
+
+ - Title describing the change
+ - Description explaining the change in detail
+ - Tests
+
+- A maintainer will respond to Pull Requests with one of:
+
+ - 'Ship It', 'LGTM', 🚢, or some other affirmation
+ - What must be changed
+ - Won't accept and why
+
+Nota Bene
++++++++++
+
+- Submitting a `draft pull request`_ is a good way to get feedback from
+ maintainers if you are unsure of the changes you are making.
+- A pull request that has been approved may not be merged immediately.
+- You may be asked to rebase or squash your commits to keep an orderly version
+ control history.
+
+.. _version control:
+
+Using Version Control
+~~~~~~~~~~~~~~~~~~~~~
+
+- `Fork`_ the `central repository`_ and work from a clone of your own fork.
+- Follow the `topic branch`_ model and submit pull requests from branches named
+ according to their purpose.
+- Review the `GitHub Flow`_ documentation and, in general, try to stick to the
+ principles outlined there.
+
+.. _writing code:
+
+Writing Code
+------------
+
+Writing code is a creative process and there will always be exceptions to the
+rules, but it's good to maintain certain standards. In general, please follow
+these code conventions.
+
+Coding Style
+~~~~~~~~~~~~
+
+- Code in this project **must** be formatted with `black`_.
+- Code in this project **must** be linted with `flake8`_.
+- Try to follow :pep:`8` guidelines.
+- Try to respect the style of existing code.
+
+Coding style checks are bundled into the static analysis automation in this
+repository's `tox`_ configuration. To validate your coding style run
+
+.. code-block:: sh
+
+ tox -e static
+
+Test Environment
+~~~~~~~~~~~~~~~~
+
+- Code **must** be tested. Write or update related unit tests so you don't have
+ to manually retest the same thing many times.
+- Tests for this project are written using the `pytest`_ framework and executed
+ via `tox`_. While it isn't always achievable this project strives to maintain
+ 💯% test coverage.
+- In addition to unit testing code in this project is statically type checked
+ using `mypy`_, formatted with `black`_, linted using `flake8`_, and security
+ checked with `bandit`_.
+
+Here are some example invocations for running unit tests/static analysis.
+
+.. code-block:: sh
+
+ tox # Build and test the project
+ tox -e py39 # in a specific environment
+ tox -e py39 -- --pdb # with extra options,
+ # or
+ tox -e py39 --devenv venv # create a development environment
+ venv/bin/python -V # and call scripts/binaries in it.
+
+Documentation
+~~~~~~~~~~~~~
+
+- Public interfaces **must** be thoroughly documented. At a minimum this
+ includes inputs, return types, exceptions raised, and surprising behavior
+ like state changes.
+- Documentation for this project is written in `reStructuredText`_ with the
+ `Google style`_ and generated with `Sphinx`_.
+
+To generate documentation run
+
+.. code-block:: sh
+
+ tox -e docs
+
+
+.. _Create an issue: https://help.github.com/articles/creating-an-issue
+.. _pull request: https://help.github.com/articles/using-pull-requests/
+.. _draft pull request: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests
+.. _topic branch: https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#Topic-Branches
+.. _Fork: https://help.github.com/articles/fork-a-repo/
+.. _central repository: https://github.com/reillysiemens/ipython-style-gruvbox/
+.. _GitHub Flow: https://guides.github.com/introduction/flow/
+.. _pytest: https://docs.pytest.org/en/latest/
+.. _tox: https://tox.readthedocs.io/en/latest/
+.. _mypy: http://www.mypy-lang.org/
+.. _black: https://black.readthedocs.io/en/stable/
+.. _flake8: https://flake8.pycqa.org/en/latest/
+.. _bandit: https://bandit.readthedocs.io/en/latest/
+.. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
+.. _Google style: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
+.. _Sphinx: http://www.sphinx-doc.org/en/master/index.html
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..da21233
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright © 2020, Reilly Tucker Siemens
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f19bf02
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,19 @@
+IPython Style Gruvbox
+=====================
+
+.. image:: https://img.shields.io/github/workflow/status/reillysiemens/ipython-style-gruvbox/Tests/master.svg?style=flat-square&label=tests
+ :target: https://github.com/reillysiemens/ipython-style-gruvbox/actions?query=workflow%3ATests
+ :alt: GitHub Actions test status
+
+.. image:: https://img.shields.io/coveralls/github/reillysiemens/ipython-style-gruvbox/master?style=flat-square
+ :target: https://coveralls.io/github/reillysiemens/ipython-style-gruvbox
+ :alt: Coveralls code coverage
+
+.. image:: https://img.shields.io/badge/code%20style-black-black?style=flat-square
+ :target: https://github.com/psf/black
+ :alt: Any color you like
+
+An opinionated terminal colorscheme for IPython using gruvbox colors.
+
+.. image:: docs/static/ipython-style-gruvbox.png
+ :alt: IPython Style Gruvbox
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..76888fe
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,3 @@
+.. _changelog:
+
+.. include:: ../CHANGELOG.rst
diff --git a/docs/code-of-conduct.rst b/docs/code-of-conduct.rst
new file mode 100644
index 0000000..bb240d1
--- /dev/null
+++ b/docs/code-of-conduct.rst
@@ -0,0 +1,3 @@
+.. _code-of-conduct:
+
+.. include:: ../CODE_OF_CONDUCT.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..ddd8ed7
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,72 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+import datetime as dt
+from pathlib import Path
+import sys
+
+sys.path.insert(0, str(Path("..").resolve()))
+
+from gruvbox import __author__, __version__ # noqa: E402
+
+
+# -- Project information -----------------------------------------------------
+
+changelog_mtime = Path("../CHANGELOG.rst").stat().st_mtime
+copyright_year = dt.datetime.utcfromtimestamp(changelog_mtime)
+homepage = "https://tuckersiemens.com"
+
+project = "ipython-style-gruvbox"
+author = __author__
+release = __version__
+version = ".".join(release.split(".", 2)[:2])
+copyright = f'{copyright_year:%Y}, {author}'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.todo",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.githubpages",
+ "sphinx.ext.napoleon",
+ "sphinx_autodoc_typehints",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "alabaster"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..4628182
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1,3 @@
+.. _contributing:
+
+.. include:: ../CONTRIBUTING.rst
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..ce0bfe0
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,33 @@
+.. ipython-style-gruvbox documentation master file
+
+IPython Style Gruvbox
+=====================
+
+Release v\ |release|. (:ref:`Changelog `)
+
+.. image:: https://img.shields.io/github/workflow/status/reillysiemens/ipython-style-gruvbox/Tests/master.svg?style=flat-square&label=tests
+ :target: https://github.com/reillysiemens/ipython-style-gruvbox/actions?query=workflow%3ATests
+ :alt: GitHub Actions test status
+
+.. image:: https://img.shields.io/coveralls/github/reillysiemens/ipython-style-gruvbox/master?style=flat-square
+ :target: https://coveralls.io/github/reillysiemens/ipython-style-gruvbox
+ :alt: Coveralls code coverage
+
+.. image:: https://img.shields.io/badge/code%20style-black-black?style=flat-square
+ :target: https://github.com/psf/black
+ :alt: Any color you like
+
+An opinionated terminal colorscheme for IPython using gruvbox colors.
+
+.. image:: static/ipython-style-gruvbox.png
+ :alt: IPython Style Gruvbox
+
+Project Info
+------------
+
+.. toctree::
+ :maxdepth: 1
+
+ Code of Conduct
+ changelog
+ contributing
diff --git a/docs/static/ipython-style-gruvbox.png b/docs/static/ipython-style-gruvbox.png
new file mode 100644
index 0000000..c7114e8
Binary files /dev/null and b/docs/static/ipython-style-gruvbox.png differ
diff --git a/gruvbox.py b/gruvbox.py
new file mode 100644
index 0000000..24c519a
--- /dev/null
+++ b/gruvbox.py
@@ -0,0 +1,108 @@
+"""An opinionated terminal colorscheme for IPython using gruvbox colors.
+
+Inspired or informed by the following:
+ - Pavel Pertsev's gruvbox colorscheme: https://git.io/JvV4I
+ - Dave Yarwood's gruvbox Pygments style: https://git.io/JvV4k
+ - python-mode's syntax highlighting: https://git.io/JvV4t
+ - The Pygments Python lexer: https://git.io/Jviis
+"""
+
+from dataclasses import dataclass
+from typing import Any, Dict
+
+from pygments.style import Style # type: ignore
+from pygments.token import ( # type: ignore
+ Comment,
+ Error,
+ Keyword,
+ Name,
+ Number,
+ Operator,
+ String,
+ Text,
+ Token,
+)
+
+__author__ = "Reilly Tucker Siemens"
+__email__ = "reilly@tuckersiemens.com"
+__version__ = "1.0.0"
+
+
+@dataclass(frozen=True)
+class Color:
+ """Absolute colors as defined by gruvbox: https://git.io/JvV8i."""
+
+ dark0_hard: str = "#1d2021"
+ dark0: str = "#282828"
+ dark0_soft: str = "#32302f"
+ dark1: str = "#3c3836"
+ dark2: str = "#504945"
+ dark3: str = "#665c54"
+ dark4: str = "#7c6f64"
+ dark4_256: str = "#7c6f64"
+
+ gray_245: str = "#928374"
+ gray_244: str = "#928374"
+
+ light0_hard: str = "#f9f5d7"
+ light0: str = "#fbf1c7"
+ light0_soft: str = "#f2e5bc"
+ light1: str = "#ebdbb2"
+ light2: str = "#d5c4a1"
+ light3: str = "#bdae93"
+ light4: str = "#a89984"
+ light4_256: str = "#a89984"
+
+ bright_red: str = "#fb4934"
+ bright_green: str = "#b8bb26"
+ bright_yellow: str = "#fabd2f"
+ bright_blue: str = "#83a598"
+ bright_purple: str = "#d3869b"
+ bright_aqua: str = "#8ec07c"
+ bright_orange: str = "#fe8019"
+
+ neutral_red: str = "#cc241d"
+ neutral_green: str = "#98971a"
+ neutral_yellow: str = "#d79921"
+ neutral_blue: str = "#458588"
+ neutral_purple: str = "#b16286"
+ neutral_aqua: str = "#689d6a"
+ neutral_orange: str = "#d65d0e"
+
+ faded_red: str = "#9d0006"
+ faded_green: str = "#79740e"
+ faded_yellow: str = "#b57614"
+ faded_blue: str = "#076678"
+ faded_purple: str = "#8f3f71"
+ faded_aqua: str = "#427b58"
+ faded_orange: str = "#af3a03"
+
+
+class GruvboxStyle(Style):
+ """An opinionated terminal colorscheme for IPython using gruvbox colors."""
+
+ styles: Dict[Any, str] = {
+ Comment: Color.gray_245,
+ Error: Color.bright_red,
+ Keyword.Namespace: Color.bright_blue,
+ Keyword.Constant: Color.bright_orange,
+ Keyword.Type: Color.bright_yellow,
+ Keyword: Color.bright_red,
+ Name.Builtin.Pseudo: Color.bright_blue,
+ Name.Builtin: Color.bright_yellow,
+ Name.Class: Color.bright_yellow,
+ Name.Decorator: f"{Color.bright_green} bold",
+ Name.Exception: Color.bright_red,
+ Name.Function: Color.bright_aqua,
+ Name.Variable.Magic: Color.bright_orange,
+ Name: Color.light1,
+ Number: Color.bright_purple,
+ Operator.Word: Color.bright_red,
+ Operator: Color.light1,
+ String.Affix: Color.light1,
+ String.Escape: Color.bright_orange,
+ String.Interpol: Color.bright_orange,
+ String: Color.bright_green,
+ Text: Color.light1,
+ Token.Punctuation: Color.light1,
+ }
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..9640edc
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,211 @@
+[[package]]
+category = "dev"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+name = "appdirs"
+optional = false
+python-versions = "*"
+version = "1.4.3"
+
+[[package]]
+category = "dev"
+description = "Cross-platform colored terminal text."
+marker = "platform_system == \"Windows\""
+name = "colorama"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "0.4.3"
+
+[[package]]
+category = "dev"
+description = "Distribution utilities"
+name = "distlib"
+optional = false
+python-versions = "*"
+version = "0.3.0"
+
+[[package]]
+category = "dev"
+description = "A platform independent file lock."
+name = "filelock"
+optional = false
+python-versions = "*"
+version = "3.0.12"
+
+[[package]]
+category = "dev"
+description = "Core utilities for Python packages"
+name = "packaging"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "20.3"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+six = "*"
+
+[[package]]
+category = "dev"
+description = "plugin and hook calling mechanisms for python"
+name = "pluggy"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "0.13.1"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+category = "dev"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+name = "py"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.8.1"
+
+[[package]]
+category = "main"
+description = "Pygments is a syntax highlighting package written in Python."
+name = "pygments"
+optional = false
+python-versions = ">=3.5"
+version = "2.6.1"
+
+[[package]]
+category = "dev"
+description = "Python parsing module"
+name = "pyparsing"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "2.4.6"
+
+[[package]]
+category = "dev"
+description = "Python 2 and 3 compatibility utilities"
+name = "six"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "1.14.0"
+
+[[package]]
+category = "dev"
+description = "Python Library for Tom's Obvious, Minimal Language"
+name = "toml"
+optional = false
+python-versions = "*"
+version = "0.10.0"
+
+[[package]]
+category = "dev"
+description = "tox is a generic virtualenv management and test command line tool"
+name = "tox"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+version = "3.14.5"
+
+[package.dependencies]
+colorama = ">=0.4.1"
+filelock = ">=3.0.0,<4"
+packaging = ">=14"
+pluggy = ">=0.12.0,<1"
+py = ">=1.4.17,<2"
+six = ">=1.14.0,<2"
+toml = ">=0.9.4"
+virtualenv = ">=16.0.0"
+
+[package.extras]
+docs = ["sphinx (>=2.0.0,<3)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"]
+testing = ["freezegun (>=0.3.11,<1)", "pathlib2 (>=2.3.3,<3)", "pytest (>=4.0.0,<6)", "pytest-cov (>=2.5.1,<3)", "pytest-mock (>=1.10.0,<2)", "pytest-xdist (>=1.22.2,<2)", "pytest-randomly (>=1.0.0,<4)", "flaky (>=3.4.0,<4)", "psutil (>=5.6.1,<6)"]
+
+[[package]]
+category = "dev"
+description = "Seamless integration of tox into GitHub Actions"
+name = "tox-gh-actions"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+version = "1.1.0"
+
+[package.dependencies]
+tox = ">=3.12.2"
+
+[package.extras]
+testing = ["flake8 (>=3,<4)", "pytest (>=4.0.0,<6)", "pytest-mock (>=2,<3)", "pytest-randomly (>=3)"]
+
+[[package]]
+category = "dev"
+description = "Virtual Python Environment builder"
+name = "virtualenv"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+version = "20.0.10"
+
+[package.dependencies]
+appdirs = ">=1.4.3,<2"
+distlib = ">=0.3.0,<1"
+filelock = ">=3.0.0,<4"
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"]
+testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.13,<1)"]
+
+[metadata]
+content-hash = "094b037700ec57d935981cb3673f973bc24c229ec01be6fb485623670c160196"
+python-versions = "^3.8"
+
+[metadata.files]
+appdirs = [
+ {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
+ {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
+]
+colorama = [
+ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
+ {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
+]
+distlib = [
+ {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"},
+]
+filelock = [
+ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
+ {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
+]
+packaging = [
+ {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
+ {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
+]
+pluggy = [
+ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+py = [
+ {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
+ {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
+]
+pygments = [
+ {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
+ {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
+]
+pyparsing = [
+ {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"},
+ {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"},
+]
+six = [
+ {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
+ {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
+]
+toml = [
+ {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
+ {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
+ {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
+]
+tox = [
+ {file = "tox-3.14.5-py2.py3-none-any.whl", hash = "sha256:0cbe98369081fa16bd6f1163d3d0b2a62afa29d402ccfad2bd09fb2668be0956"},
+ {file = "tox-3.14.5.tar.gz", hash = "sha256:676f1e3e7de245ad870f956436b84ea226210587d1f72c8dfb8cd5ac7b6f0e70"},
+]
+tox-gh-actions = [
+ {file = "tox-gh-actions-1.1.0.tar.gz", hash = "sha256:fbce2a435cab4619e5fe77ce08b941bf8f1569f67cc43ac26b0249b2c7b029f4"},
+ {file = "tox_gh_actions-1.1.0-py2.py3-none-any.whl", hash = "sha256:1c491a6e73b13c72829b95e50d63060f920bd02e5e2ba3c5a79bfc312598f966"},
+]
+virtualenv = [
+ {file = "virtualenv-20.0.10-py2.py3-none-any.whl", hash = "sha256:10750cac3b5a9e6eed54d0f1f8222c550dc47f84609c95cbc504d44a58a048b8"},
+ {file = "virtualenv-20.0.10.tar.gz", hash = "sha256:8512e83f1d90f8e481024d58512ac9c053bf16f54d9138520a0929396820dd78"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..a54ffd7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,87 @@
+[build-system]
+requires = ["poetry>=0.12"]
+build-backend = "poetry.masonry.api"
+
+[tool.poetry]
+name = "ipython-style-gruvbox"
+version = "1.0.0"
+description = "An opinionated terminal colorscheme for IPython using gruvbox colors."
+authors = ["Reilly Tucker Siemens "]
+license = "ISC"
+packages = [
+ { include = "gruvbox.py" },
+]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+pygments = "^2.6.1"
+
+[tool.poetry.dev-dependencies]
+tox = "^3.14.5"
+tox-gh-actions = "^1.1.0"
+
+[tool.poetry.plugins."pygments.styles"]
+gruvbox = "gruvbox:GruvboxStyle"
+
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+minversion = 3.14.0
+envlist =
+ py38
+ static
+ docs
+
+[testenv]
+whitelist_externals = poetry
+skip_install = true
+deps =
+ pytest ~= 5.3.0
+ pytest-cov ~= 2.8.0
+ pytest-randomly ~= 3.2.0
+# XXX: (2020-03-15) This will install tox again, but it only happens once.
+commands_pre = poetry install
+commands =
+ pytest \
+ --verbose \
+ --cov=gruvbox \
+ --cov-report=term-missing \
+ --cov-fail-under=100 \
+ {posargs}
+
+[testenv:static]
+basepython = python3.8
+deps =
+ bandit ~= 1.6.0
+ black ~= 19.10b0
+ flake8 ~= 3.7.0
+ flake8-bugbear ~= 20.1.0
+ flake8-commas ~= 2.0.0
+ flake8-docstrings ~= 1.5.0
+ pep8-naming ~= 0.9.0
+ mypy ~= 0.760
+commands_pre =
+commands =
+ black --check gruvbox.py
+ flake8 --docstring-convention=google gruvbox.py
+ mypy \
+ --disallow-untyped-calls \
+ --disallow-untyped-defs \
+ --disallow-incomplete-defs \
+ --disallow-untyped-decorators \
+ gruvbox.py
+ bandit --recursive -ll gruvbox.py
+
+[testenv:docs]
+basepython = python3.8
+deps =
+ sphinx ~= 2.3.0
+ sphinx-autodoc-typehints ~= 1.10.0
+commands_pre =
+commands =
+ sphinx-build -b html docs {posargs:docs/_build/html}
+
+[gh-actions]
+python =
+ 3.8: py38, static, docs
+"""
diff --git a/tests/test_gruvbox.py b/tests/test_gruvbox.py
new file mode 100644
index 0000000..2c00fc3
--- /dev/null
+++ b/tests/test_gruvbox.py
@@ -0,0 +1,140 @@
+import pygments
+from pygments.lexers import PythonLexer
+from pygments.formatters import TerminalTrueColorFormatter
+import pytest
+from pytest import param
+
+from gruvbox import GruvboxStyle
+
+
+def highlight(code: str) -> str:
+ """Highlight code in true color (24-bit) with the GruvboxStyle."""
+ return pygments.highlight(
+ code=code,
+ lexer=PythonLexer(),
+ formatter=TerminalTrueColorFormatter(style=GruvboxStyle),
+ )
+
+
+@pytest.mark.parametrize(
+ "code,expected",
+ [
+ param(
+ "# This is a comment.",
+ "\x1b[38;2;146;131;116m# This is a comment.\x1b[39m\n",
+ id="Comment",
+ ),
+ param(
+ "err?",
+ "\x1b[38;2;235;219;178merr\x1b[39m\x1b[38;2;251;73;52m?\x1b[39m\n",
+ id="Error",
+ ),
+ param(
+ "from gruvbox import GruvboxStyle",
+ "\x1b[38;2;131;165;152mfrom\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mgruvbox\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;131;165;152mimport\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mGruvboxStyle\x1b[39m\n", # noqa: E501
+ id="Keyword.Namespace",
+ ),
+ param("None", "\x1b[38;2;254;128;25mNone\x1b[39m\n", id="Keyword.Constant",),
+ param("int", "\x1b[38;2;250;189;47mint\x1b[39m\n", id="Keyword.Type",),
+ param(
+ "try:\n pass\nexcept:\n raise",
+ "\x1b[38;2;251;73;52mtry\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\n\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mpass\x1b[39m\n\x1b[38;2;251;73;52mexcept\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\n\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mraise\x1b[39m\n", # noqa: E501
+ id="Keyword",
+ ),
+ param(
+ "def method(self, other): ...",
+ "\x1b[38;2;251;73;52mdef\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;142;192;124mmethod\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;131;165;152mself\x1b[39m\x1b[38;2;235;219;178m,\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mother\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\n", # noqa: E501
+ id="Name.Builtin.Pseudo",
+ ),
+ param(
+ "list(map(lambda n: n + 1, range(5)))",
+ "\x1b[38;2;250;189;47mlist\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;250;189;47mmap\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;251;73;52mlambda\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mn\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mn\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m+\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m1\x1b[39m\x1b[38;2;235;219;178m,\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;250;189;47mrange\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;211;134;155m5\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\n", # noqa: E501
+ id="Name.Builtin",
+ ),
+ param(
+ "class IsDismissed: ...",
+ "\x1b[38;2;251;73;52mclass\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;250;189;47mIsDismissed\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\n", # noqa: E501
+ id="Name.Class",
+ ),
+ param(
+ "@decorator\ndef function(): ...",
+ "\x1b[38;2;184;187;38;01m@decorator\x1b[39;00m\n\x1b[38;2;251;73;52mdef\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;142;192;124mfunction\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\n", # noqa: E501
+ id="Name.Decorator",
+ ),
+ param(
+ 'raise KeyError("Wrong lock.")',
+ '\x1b[38;2;251;73;52mraise\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mKeyError\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;184;187;38mWrong lock.\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\n', # noqa: E501
+ id="Name.Exception",
+ ),
+ param(
+ "def foo(bar: int, baz: Optional[str] = None) -> None: ...",
+ "\x1b[38;2;251;73;52mdef\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;142;192;124mfoo\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;235;219;178mbar\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;250;189;47mint\x1b[39m\x1b[38;2;235;219;178m,\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mbaz\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mOptional\x1b[39m\x1b[38;2;235;219;178m[\x1b[39m\x1b[38;2;250;189;47mstr\x1b[39m\x1b[38;2;235;219;178m]\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m=\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;254;128;25mNone\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m-\x1b[39m\x1b[38;2;235;219;178m>\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;254;128;25mNone\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\n", # noqa: E501
+ id="Name.Function",
+ ),
+ param(
+ 'class IsDismissed:\n __slots__ = ("attr",)',
+ '\x1b[38;2;251;73;52mclass\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;250;189;47mIsDismissed\x1b[39m\x1b[38;2;235;219;178m:\x1b[39m\n\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;254;128;25m__slots__\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m=\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;184;187;38mattr\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;235;219;178m,\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\n', # noqa: E501
+ id="Name.Variable.Magic",
+ ),
+ param(
+ "life = 42",
+ "\x1b[38;2;235;219;178mlife\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m=\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m42\x1b[39m\n", # noqa: E501
+ id="Name",
+ ),
+ param(
+ "0b1101001", "\x1b[38;2;211;134;155m0b1101001\x1b[39m\n", id="Number.Bin",
+ ),
+ param("105.0", "\x1b[38;2;211;134;155m105.0\x1b[39m\n", id="Number.Float",),
+ param("0x69", "\x1b[38;2;211;134;155m0x69\x1b[39m\n", id="Number.Hex",),
+ param("105", "\x1b[38;2;211;134;155m105\x1b[39m\n", id="Number.Integer",),
+ param("0o151", "\x1b[38;2;211;134;155m0o151\x1b[39m\n", id="Number.Oct",),
+ param(
+ "x is not y and w is (y or z)",
+ "\x1b[38;2;235;219;178mx\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mis\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mnot\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178my\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mand\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mw\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mis\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;235;219;178my\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;251;73;52mor\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178mz\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\n", # noqa: E501
+ id="Operator.Word",
+ ),
+ param(
+ "3 != 4, 5 == 5, 0b001 << 3",
+ "\x1b[38;2;211;134;155m3\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m!=\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m4\x1b[39m\x1b[38;2;235;219;178m,\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m5\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m==\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m5\x1b[39m\x1b[38;2;235;219;178m,\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m0b001\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m<<\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;211;134;155m3\x1b[39m\n", # noqa: E501
+ id="Operator",
+ ),
+ param(
+ 'f"f-strings {rule}"',
+ '\x1b[38;2;235;219;178mf\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;184;187;38mf-strings \x1b[39m\x1b[38;2;254;128;25m{\x1b[39m\x1b[38;2;235;219;178mrule\x1b[39m\x1b[38;2;254;128;25m}\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\n', # noqa: E501
+ id="String.Affix",
+ ),
+ param(
+ '"""This is a docstring."""',
+ '\x1b[38;2;184;187;38m"""This is a docstring."""\x1b[39m\n',
+ id="String.Doc",
+ ),
+ param(
+ '"\\n\\t"',
+ '\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;254;128;25m\\n\x1b[39m\x1b[38;2;254;128;25m\\t\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\n', # noqa: E501
+ id="String.Escape",
+ ),
+ param(
+ '"%s" % "{xyzzy}".format(xyzzy="plugh")',
+ '\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;254;128;25m%s\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;235;219;178m%\x1b[39m\x1b[38;2;235;219;178m \x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;254;128;25m{xyzzy}\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178mformat\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;235;219;178mxyzzy\x1b[39m\x1b[38;2;235;219;178m=\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;184;187;38mplugh\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\n', # noqa: E501
+ id="String.Interpol",
+ ),
+ param(
+ '"This is a string."',
+ '\x1b[38;2;184;187;38m"\x1b[39m\x1b[38;2;184;187;38mThis is a string.\x1b[39m\x1b[38;2;184;187;38m"\x1b[39m\n', # noqa: E501
+ id="String",
+ ),
+ param(
+ " \n ",
+ "\x1b[38;2;235;219;178m \x1b[39m\n\x1b[38;2;235;219;178m \x1b[39m\n",
+ id="Text",
+ ),
+ param(
+ "[].reverse()",
+ "\x1b[38;2;235;219;178m[\x1b[39m\x1b[38;2;235;219;178m]\x1b[39m\x1b[38;2;235;219;178m.\x1b[39m\x1b[38;2;235;219;178mreverse\x1b[39m\x1b[38;2;235;219;178m(\x1b[39m\x1b[38;2;235;219;178m)\x1b[39m\n", # noqa: E501
+ id="Token.Punctuation",
+ ),
+ ],
+)
+def test_highlighting(code: str, expected: str) -> None:
+ """The given code should highlight as expected."""
+ assert highlight(code) == expected