[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Test and lint\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [ \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\" ]\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: pip\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip setuptools wheel coverage[toml]\n      - name: Run tests\n        run: |\n          coverage run -m unittest -v\n          coverage report\n      - name: Install linting tools\n        run: |\n          pip install black mypy\n      - name: Run style check\n        run: |\n          black --check pychord test\n      - name: Run type check\n        run: |\n          mypy pychord\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: CodeQL\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  schedule:\n    - cron: '0 2 * * 1'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        language:\n          - python\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v3\n        with:\n          languages: ${{ matrix.language }}\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@v3\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v3\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Publish Python package\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install build twine\n      - name: Build and publish\n        env:\n          TWINE_USERNAME: __token__\n          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}\n        run: |\n          python -m build\n          twine upload dist/*\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\nvenv/\n.DS_Store\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\nmake.bat\n\n# PyBuilder\ntarget/\n\n#Ipython Notebook\n.ipynb_checkpoints\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-lts-latest\n  tools:\n    python: \"3.12\"\n\nsphinx:\n  configuration: docs/conf.py\n\npython:\n  install:\n    - requirements: docs/requirements.txt\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![PyChord](https://github.com/yuma-m/pychord/raw/main/pychord.png)\n\n# PyChord ![Build Status](https://github.com/yuma-m/pychord/actions/workflows/build.yml/badge.svg) [![Documentation Status](https://readthedocs.org/projects/pychord/badge/?version=latest)](http://pychord.readthedocs.io/en/latest/?badge=latest)\n\n## Overview\n\nPyChord is a Python library to handle musical chords.\n\n## Installation\n\nPyChord supports Python 3.10 and above.\n\n```sh\n$ pip install pychord\n```\n\n## Basic Usage\n\n### Create a Chord\n\n```python\n>>> from pychord import Chord\n>>> c = Chord(\"Am7\")\n>>> c\n<Chord: Am7>\n>>> c.info()\n\"\"\"\nAm7\nroot=A\nquality=m7\non=None\n\"\"\"\n```\n\n### Transpose a Chord\n\n```python\n>>> c = Chord(\"Am7/G\")\n>>> c.transpose(3)\n>>> c\n<Chord: Cm7/Bb>\n```\n\n### Get component notes\n\n```python\n>>> c = Chord(\"Am7\")\n>>> c.components()\n['A', 'C', 'E', 'G']\n>>> c.components_with_pitch(root_pitch=3)\n['A3', 'C4', 'E4', 'G4']\n```\n\n### Compare Chords\n\n```python\n>>> Chord(\"C\") == Chord(\"D\")\nFalse\n>>> Chord(\"C#\") == Chord(\"Db\")\nTrue\n>>> c = Chord(\"C\")\n>>> c.transpose(2)\n>>> c == Chord(\"D\")\nTrue\n```\n\n### Find Chords from notes\n\n```python\n>>> from pychord import find_chords_from_notes\n>>> find_chords_from_notes([\"C\", \"E\", \"G\"])\n[ <Chord: C>]\n>>> find_chords_from_notes([\"F#\", \"A\", \"C\", \"D\"])\n[ <Chord: D7/F#>]\n>>> find_chords_from_notes([\"F\", \"G\", \"C\"])\n[ <Chord: Fsus2>, <Chord: Csus4/F>]\n```\n\n### Create and handle chord progressions\n\n```python\n>>> from pychord import ChordProgression\n>>> cp = ChordProgression([\"C\", \"G/B\", \"Am\"])\n>>> cp\n<ChordProgression: C | G/B | Am>\n\n>>> cp.append(\"Em/G\")\n>>> cp\n<ChordProgression: C | G/B | Am | Em/G>\n\n>>> cp.transpose(+3)\n>>> cp\n<ChordProgression: Eb | Bb/D | Cm | Gm/Bb>\n\n>>> cp[1]\n<Chord: Bb/D>\n```\n\n## Advanced Usage\n\n### Create a Chord from note index in a scale\n\n```python\n>>> Chord.from_note_index(note=1, quality=\"\", scale=\"Cmaj\")\n<Chord: C>  # I of C major\n>>> Chord.from_note_index(note=3, quality=\"m7\", scale=\"Fmaj\")\n<Chord: Am7>  # IIIm7 of F major\n>>> Chord.from_note_index(note=5, quality=\"7\", scale=\"Amin\")\n<Chord: E7>  # V7 of A minor\n```\n\n### Overwrite the default Quality components with yours\n\n```python\n>>> from pychord import Chord, QualityManager\n>>> Chord(\"C11\").components()\n['C', 'E', 'G', 'Bb', 'D', 'F']\n\n>>> quality_manager = QualityManager()\n>>> quality_manager.set_quality(\"11\", (\"1\", \"3\", \"5\", \"b7\", \"11\"))\n>>> Chord(\"C11\").components()\n['C', 'E', 'G', 'Bb', 'F']\n```\n\n### Inversions\n\nChord inversions are created with a forward slash and a number\nindicating the order. This can optionally be combined with an\nadditional forward slash to change the bass note:\n\n```python\n>>> Chord(\"C/1\").components() # First inversion of C\n['E', 'G', 'C']\n>>> Chord(\"C/2\").components() # Second inversion of C\n['G', 'C', 'E']\n\n>>> Chord(\"Cm7/3/F\").components() # Third inversion of Cm7 with an added F bass\n['F', 'Bb', 'C', 'Eb', 'G']\n```\n\n## Examples\n\n- [pychord-midi.py](./examples/pychord-midi.py) - Create a MIDI file using PyChord and pretty_midi.\n\n## Links\n\n- [PyPI](https://pypi.python.org/pypi/pychord)\n- [GitHub](https://github.com/yuma-m/pychord)\n- [Documentation](http://pychord.readthedocs.io/en/latest/)\n\n## Author\n\n- [Yuma Mihira](https://yuma.cloud/)\n\n## License\n\n- MIT License\n\nThe logo is made from [Freepik](https://www.flaticon.com/authors/freepik).\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_elements.papersize=a4\nPAPEROPT_letter = -D latex_elements.papersize=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html        to make standalone HTML files\"\n\t@echo \"  dirhtml     to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml  to make a single large HTML file\"\n\t@echo \"  pickle      to make pickle files\"\n\t@echo \"  json        to make JSON files\"\n\t@echo \"  htmlhelp    to make HTML files and an HTML help project\"\n\t@echo \"  qthelp      to make HTML files and a qthelp project\"\n\t@echo \"  applehelp   to make an Apple Help Book\"\n\t@echo \"  devhelp     to make HTML files and a Devhelp project\"\n\t@echo \"  epub        to make an epub\"\n\t@echo \"  epub3       to make an epub3\"\n\t@echo \"  latex       to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf    to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja  to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  lualatexpdf to make LaTeX files and run them through lualatex\"\n\t@echo \"  xelatexpdf  to make LaTeX files and run them through xelatex\"\n\t@echo \"  text        to make text files\"\n\t@echo \"  man         to make manual pages\"\n\t@echo \"  texinfo     to make Texinfo files\"\n\t@echo \"  info        to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext     to make PO message catalogs\"\n\t@echo \"  changes     to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml         to make Docutils-native XML files\"\n\t@echo \"  pseudoxml   to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck   to check all external links for integrity\"\n\t@echo \"  doctest     to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage    to run coverage check of the documentation (if enabled)\"\n\t@echo \"  dummy       to check syntax errors of document sources\"\n\n.PHONY: clean\nclean:\n\trm -rf $(BUILDDIR)/*\n\n.PHONY: html\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\n.PHONY: dirhtml\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\n.PHONY: singlehtml\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\n.PHONY: pickle\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\n.PHONY: json\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\n.PHONY: htmlhelp\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\n.PHONY: qthelp\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/pychord.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/pychord.qhc\"\n\n.PHONY: applehelp\napplehelp:\n\t$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp\n\t@echo\n\t@echo \"Build finished. The help book is in $(BUILDDIR)/applehelp.\"\n\t@echo \"N.B. You won't be able to view it unless you put it in\" \\\n\t      \"~/Library/Documentation/Help or install it in your application\" \\\n\t      \"bundle.\"\n\n.PHONY: devhelp\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/pychord\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pychord\"\n\t@echo \"# devhelp\"\n\n.PHONY: epub\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\n.PHONY: epub3\nepub3:\n\t$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3\n\t@echo\n\t@echo \"Build finished. The epub3 file is in $(BUILDDIR)/epub3.\"\n\n.PHONY: latex\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\n.PHONY: latexpdf\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: latexpdfja\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: lualatexpdf\nlualatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through lualatex...\"\n\t$(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf\n\t@echo \"lualatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: xelatexpdf\nxelatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through xelatex...\"\n\t$(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf\n\t@echo \"xelatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: text\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\n.PHONY: man\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\n.PHONY: texinfo\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\n.PHONY: info\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\n.PHONY: gettext\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\n.PHONY: changes\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\n.PHONY: linkcheck\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\n.PHONY: doctest\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\n.PHONY: coverage\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\n.PHONY: xml\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\n.PHONY: pseudoxml\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n\n.PHONY: dummy\ndummy:\n\t$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy\n\t@echo\n\t@echo \"Build finished. Dummy builder generates no files.\"\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# pychord documentation build configuration file, created by\n# sphinx-quickstart on Sat Dec 31 14:51:42 2016.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\n\nsys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.todo',\n    'sphinx.ext.viewcode']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\nsource_suffix = ['.rst', '.md']\n# source_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'pychord'\ncopyright = u'2016 - 2026, Yuma Mihira'\nauthor = u'Yuma Mihira'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u''\n# The full version, including alpha/beta/rc tags.\nrelease = u''\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = True\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'pychorddoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'pychord.tex', u'pychord Documentation',\n     u'Author', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'pychord', u'pychord Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'pychord', u'pychord Documentation',\n     author, 'pychord', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n# -- Options for Epub output ----------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\nepub_author = author\nepub_publisher = author\nepub_copyright = copyright\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = ['search.html']\n\n\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. pychord documentation master file, created by\n   sphinx-quickstart on Sat Dec 31 14:51:42 2016.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to pychord's documentation!\n===================================\n\n.. toctree::\n   :maxdepth: 4\n   :caption: Contents:\n\n   pychord\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/pychord.rst",
    "content": "pychord package\n===============\n\n.. automodule:: pychord\n   :members:\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "sphinx==9.1.0\nsphinx_rtd_theme==3.1.0\n"
  },
  {
    "path": "examples/pychord-midi.py",
    "content": "# An example to create MIDI file with PyChord and pretty_midi\n# Prerequisite: pip install pretty_midi\n# pretty_midi: https://github.com/craffel/pretty-midi\n\n\nimport pretty_midi\n\nfrom pychord import Chord\n\n\ndef create_midi(chords):\n    midi_data = pretty_midi.PrettyMIDI()\n    piano_program = pretty_midi.instrument_name_to_program('Acoustic Grand Piano')\n    piano = pretty_midi.Instrument(program=piano_program)\n    length = 1\n    for n, chord in enumerate(chords):\n        for note_name in chord.components_with_pitch(root_pitch=4):\n            note_number = pretty_midi.note_name_to_number(note_name)\n            note = pretty_midi.Note(velocity=100, pitch=note_number, start=n * length, end=(n + 1) * length)\n            piano.notes.append(note)\n    midi_data.instruments.append(piano)\n    midi_data.write('chord.mid')\n\n\ndef main():\n    chords_str = [\"C\", \"Dm7\", \"G\", \"C\"]\n    chords = [Chord(c) for c in chords_str]\n    create_midi(chords)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "pychord/__init__.py",
    "content": "from .analyzer import find_chords_from_notes\nfrom .chord import Chord\nfrom .progression import ChordProgression\nfrom .quality import Quality, QualityManager\n\n__all__ = [\n    \"find_chords_from_notes\",\n    \"Chord\",\n    \"ChordProgression\",\n    \"Quality\",\n    \"QualityManager\",\n]\n"
  },
  {
    "path": "pychord/analyzer.py",
    "content": "from .chord import Chord\nfrom .quality import QualityManager\nfrom .utils import note_to_val\n\n\ndef find_chords_from_notes(notes: list[str]) -> list[Chord]:\n    \"\"\"\n    Find possible chords consisting of the given notes.\n\n    :param notes: List of notes arranged from lower note, e.g. ``[\"C\", \"Eb\", \"G\"]``.\n    \"\"\"\n    if not notes:\n        raise ValueError(\"Please specify notes which consist a chord.\")\n    root = notes[0]\n    root_and_positions = []\n    for rotated_notes in get_all_rotated_notes(notes):\n        rotated_root = rotated_notes[0]\n        root_and_positions.append(\n            (rotated_root, notes_to_positions(rotated_notes, rotated_notes[0]))\n        )\n    chords = []\n    for temp_root, positions in root_and_positions:\n        quality = QualityManager().find_quality_from_components(positions)\n        if quality is None:\n            continue\n        if temp_root == root:\n            chord = \"{}{}\".format(root, quality)\n        else:\n            chord = \"{}{}/{}\".format(temp_root, quality, root)\n        chords.append(Chord(chord))\n    return chords\n\n\ndef notes_to_positions(notes: list[str], root: str) -> list[int]:\n    \"\"\"\n    Get notes positions from the root note.\n\n    >>> notes_to_positions([\"C\", \"E\", \"G\"], \"C\")\n    [0, 4, 7]\n\n    :param notes: List of notes.\n    :param root: Root note.\n    \"\"\"\n    root_pos = note_to_val(root)\n    current_pos = root_pos\n    positions = []\n    for note in notes:\n        note_pos = note_to_val(note)\n        if note_pos < current_pos:\n            note_pos += 12 * ((current_pos - note_pos) // 12 + 1)\n        positions.append(note_pos - root_pos)\n        current_pos = note_pos\n    return positions\n\n\ndef get_all_rotated_notes(notes: list[str]) -> list[list[str]]:\n    \"\"\"\n    Get all rotated notes.\n\n    get_all_rotated_notes([A,C,E]) -> [[A,C,E],[C,E,A],[E,A,C]]\n    \"\"\"\n    notes_list = []\n    for x in range(len(notes)):\n        notes_list.append(notes[x:] + notes[:x])\n    return notes_list\n"
  },
  {
    "path": "pychord/chord.py",
    "content": "from typing import Any, Literal, overload\n\nfrom .constants.scales import RELATIVE_KEY_DICT\nfrom .parser import parse, parse_scale\nfrom .quality import QualityManager, Quality, scale_notes\nfrom .utils import augment, diminish, transpose_note, note_to_val\n\n\nclass Chord:\n    \"\"\"\n    A chord, made up of two or more notes.\n\n    :param chord: Name of the chord, e.g. ``\"C\"``, ``\"Am7\"``, ``\"F#m7-5/A\"``.\n    \"\"\"\n\n    def __init__(self, chord: str) -> None:\n        root, quality, on = parse(chord)\n        self._chord: str = chord\n        self._root: str = root\n        self._quality: Quality = quality\n        self._on: str = on\n\n    def __str__(self) -> str:\n        return self._chord\n\n    def __repr__(self) -> str:\n        return f\"<Chord: {self._chord}>\"\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, Chord):\n            raise TypeError(f\"Cannot compare Chord object with {type(other)} object\")\n        if note_to_val(self._root) != note_to_val(other.root):\n            return False\n        if self._quality != other.quality:\n            return False\n\n        if (\n            # If one chord has an \"on\" and not the other, they differ.\n            bool(self._on)\n            != bool(other.on)\n        ) or (\n            # If both chords have an \"on\" and they are not enharmonic, they differ.\n            self._on\n            and other.on\n            and note_to_val(self._on) != note_to_val(other.on)\n        ):\n            return False\n\n        return True\n\n    @classmethod\n    def from_note_index(\n        cls,\n        note: int,\n        quality: str,\n        scale: str,\n        diatonic: bool = False,\n        chromatic: int = 0,\n    ) -> \"Chord\":\n        \"\"\"Create a :class:`Chord` from a note index in a scale.\n\n        - ``Chord.from_note_index(1, \"\", \"Cmaj\")`` returns I of C major => Chord(\"C\")\n        - ``Chord.from_note_index(3, \"m7\", \"Fmaj\")`` returns IIImin of F major => Chord(\"Am7\")\n        - ``Chord.from_note_index(5, \"7\", \"Amin\")`` returns Vmin of A minor => Chord(\"E7\")\n        - ``Chord.from_note_index(2, \"\", \"Cmaj\")`` returns II of C major => Chord(\"D\")\n        - ``Chord.from_note_index(2, \"m\", \"Cmaj\")`` returns IImin of C major => Chord(\"Dm\")\n        - ``Chord.from_note_index(2, \"\", \"Cmaj\", diatonic=True)`` returns IImin of C major => Chord(\"Dm\")\n        - ``Chord.from_note_index(2, \"\", \"Cmin\", chromatic=-1)`` returns bII of C minor => Chord(\"Db\")\n\n        :param note: Scale degree of the chord's root, ``1`` to ``7``.\n        :param quality: Quality of the chord, e.g. ``\"m7\"``, ``\"sus4\"``.\n        :param scale: Base scale, e.g. ``\"Cmaj\"``, ``\"Amin\"``, ``\"F#maj\"``, ``\"Ebmin\"``.\n        :param diatonic: If True, chord quality is determined using the base scale (overrides ``quality``).\n        :param chromatic: Lower or raise the scale degree (and all notes of the chord) by semitone(s).\n        \"\"\"\n        if not 1 <= note <= 7:\n            raise ValueError(f\"Invalid note {note}\")\n        scale_root, scale_mode = parse_scale(scale)\n        root = scale_notes(scale_root, scale_mode)[note - 1]\n        if chromatic != 0:\n            alter = augment if chromatic > 0 else diminish\n            for i in range(abs(chromatic)):\n                root = alter(root)\n\n        if diatonic:\n            scale_degrees = RELATIVE_KEY_DICT[scale_mode]\n\n            # construct the chord based on scale degrees, within 1 octave\n            first = scale_degrees[note - 1]\n            third = scale_degrees[(note + 1) % 7]\n            fifth = scale_degrees[(note + 3) % 7]\n            seventh = scale_degrees[(note + 5) % 7]\n\n            # adjust the chord to its root position (as a stack of thirds),\n            # then set the root to 0\n            # e.g. (9, 0, 4) -> [0, 3, 7]\n            def get_diatonic_chord(chord: tuple[int, ...]) -> list[int]:\n                uninverted: list[int] = []\n                for note in chord:\n                    if not uninverted:\n                        uninverted.append(note)\n                    elif note > uninverted[-1]:\n                        uninverted.append(note)\n                    else:\n                        uninverted.append(note + 12)\n                uninverted = [x - uninverted[0] for x in uninverted]\n                return uninverted\n\n            if quality in [\"\", \"-\", \"maj\", \"m\", \"min\"]:\n                triad = (first, third, fifth)\n                q = get_diatonic_chord(triad)\n            elif quality in [\"7\", \"M7\", \"maj7\", \"m7\"]:\n                seventh_chord = (first, third, fifth, seventh)\n                q = get_diatonic_chord(seventh_chord)\n            else:\n                raise NotImplementedError(\n                    \"Only generic chords (triads, sevenths) are supported\"\n                )\n\n            # look up QualityManager to determine chord quality\n            quality_manager = QualityManager()\n            quality_instance = quality_manager.find_quality_from_components(q)\n            assert quality_instance is not None\n            quality = quality_instance.quality\n\n        return cls(f\"{root}{quality}\")\n\n    @property\n    def chord(self) -> str:\n        \"\"\"\n        The name of the chord, e.g. ``\"C\"``, ``\"Am7\"``, ``\"F#m7-5/A\"``.\n        \"\"\"\n        return self._chord\n\n    @property\n    def root(self) -> str:\n        \"\"\"\n        The root note of the chord, e.g. ``\"C\"``, ``\"A\"``, ``\"F#\"``.\n        \"\"\"\n        return self._root\n\n    @property\n    def quality(self) -> Quality:\n        \"\"\"\n        The quality of the chord, e.g. ``\"maj\"``, ``\"m7\"``, ``\"m7-5\"``.\n        \"\"\"\n        return self._quality\n\n    @property\n    def on(self) -> str:\n        \"\"\"\n        The bass note of a slash chord.\n        \"\"\"\n        return self._on\n\n    def info(self) -> str:\n        \"\"\"\n        Return information of chord to display.\n        \"\"\"\n        return f\"\"\"{self._chord}\nroot={self._root}\nquality={self._quality}\non={self._on}\"\"\"\n\n    def transpose(self, trans: int, scale: str = \"C\") -> None:\n        \"\"\"\n        Transpose the chord.\n\n        :param trans: The number of semitones.\n        :param scale: Key scale.\n        \"\"\"\n        if not isinstance(trans, int):\n            raise TypeError(f\"Expected integers, not {type(trans)}\")\n        self._root = transpose_note(self._root, trans, scale)\n        if self._on:\n            self._on = transpose_note(self._on, trans, scale)\n        self._reconfigure_chord()\n\n    @overload\n    def components(self, visible: Literal[True]) -> list[str]: ...\n\n    @overload\n    def components(self, visible: Literal[False]) -> list[int]: ...\n\n    def components(self, visible: bool = True) -> list[str] | list[int]:\n        \"\"\"\n        Return the component notes of the chord.\n\n        :param visible: Returns the note names if ``True``, the note pitches otherwise.\n        \"\"\"\n        if visible:\n            notes = self._quality.get_components(root=self._root, visible=True)\n            if self._on:\n                notes = [n for n in notes if n != self._on]\n                notes.insert(0, self._on)\n            return notes\n        else:\n            components = self._quality.get_components(root=self._root, visible=False)\n            if self._on:\n                on_value = note_to_val(self._on)\n                components = [c for c in components if c % 12 != on_value % 12]\n                if on_value > components[0]:\n                    on_value -= 12\n                components.insert(0, on_value)\n            return components\n\n    def components_with_pitch(self, root_pitch: int) -> list[str]:\n        \"\"\"\n        Return the component notes of chord formatted like ``[\"C4\", \"E4\", \"G4\"]``.\n\n        :param root_pitch: The pitch of the root note.\n        \"\"\"\n        components = self.components(visible=False)\n        notes = self.components(visible=True)\n        if components[0] < 0:\n            components = [c + 12 for c in components]\n        return [f\"{n}{root_pitch + c // 12}\" for (n, c) in zip(notes, components)]\n\n    def _reconfigure_chord(self) -> None:\n        self._chord = \"{}{}{}\".format(\n            self._root,\n            self._quality.quality,\n            f\"/{self._on}\" if self._on else \"\",\n        )\n"
  },
  {
    "path": "pychord/constants/__init__.py",
    "content": ""
  },
  {
    "path": "pychord/constants/qualities.py",
    "content": "# Do not import DEFAULT_QUALITIES directly\n# Use QualityManager instead\nDEFAULT_QUALITIES = [\n    # chords consist of 2 notes\n    (\"5\", (\"1\", \"5\")),\n    (\"no5\", (\"1\", \"3\")),\n    (\"omit5\", (\"1\", \"3\")),\n    (\"m(no5)\", (\"1\", \"b3\")),\n    (\"m(omit5)\", (\"1\", \"b3\")),\n    # 3 notes\n    (\"\", (\"1\", \"3\", \"5\")),\n    (\"maj\", (\"1\", \"3\", \"5\")),\n    (\"m\", (\"1\", \"b3\", \"5\")),\n    (\"min\", (\"1\", \"b3\", \"5\")),\n    (\"-\", (\"1\", \"b3\", \"5\")),\n    (\"dim\", (\"1\", \"b3\", \"b5\")),\n    # Not to confuse Ab5 with A(b5)\n    (\"(b5)\", (\"1\", \"3\", \"b5\")),\n    (\"aug\", (\"1\", \"3\", \"#5\")),\n    (\"sus2\", (\"1\", \"2\", \"5\")),\n    (\"sus4\", (\"1\", \"4\", \"5\")),\n    (\"sus\", (\"1\", \"4\", \"5\")),\n    # 4 notes\n    (\"6\", (\"1\", \"3\", \"5\", \"6\")),\n    # https://www.scales-chords.com/chord/piano/C%236b5\n    (\"6b5\", (\"1\", \"3\", \"b5\", \"6\")),\n    (\"6-5\", (\"1\", \"3\", \"b5\", \"6\")),\n    (\"7\", (\"1\", \"3\", \"5\", \"b7\")),\n    (\"7-5\", (\"1\", \"3\", \"b5\", \"b7\")),\n    (\"7b5\", (\"1\", \"3\", \"b5\", \"b7\")),\n    (\"7+5\", (\"1\", \"3\", \"#5\", \"b7\")),\n    (\"7#5\", (\"1\", \"3\", \"#5\", \"b7\")),\n    (\"7sus4\", (\"1\", \"4\", \"5\", \"b7\")),\n    (\"m6\", (\"1\", \"b3\", \"5\", \"6\")),\n    (\"m7\", (\"1\", \"b3\", \"5\", \"b7\")),\n    (\"m7-5\", (\"1\", \"b3\", \"b5\", \"b7\")),\n    (\"m7b5\", (\"1\", \"b3\", \"b5\", \"b7\")),\n    (\"m7+5\", (\"1\", \"b3\", \"#5\", \"b7\")),\n    (\"m7#5\", (\"1\", \"b3\", \"#5\", \"b7\")),\n    (\"dim7\", (\"1\", \"b3\", \"b5\", \"bb7\")),\n    (\"M7\", (\"1\", \"3\", \"5\", \"7\")),\n    (\"maj7\", (\"1\", \"3\", \"5\", \"7\")),\n    (\"maj7+5\", (\"1\", \"3\", \"#5\", \"7\")),\n    (\"M7+5\", (\"1\", \"3\", \"#5\", \"7\")),\n    (\"mmaj7\", (\"1\", \"b3\", \"5\", \"7\")),\n    (\"mM7\", (\"1\", \"b3\", \"5\", \"7\")),\n    (\"add4\", (\"1\", \"3\", \"4\", \"5\")),\n    (\"majadd4\", (\"1\", \"3\", \"4\", \"5\")),\n    (\"Madd4\", (\"1\", \"3\", \"4\", \"5\")),\n    (\"madd4\", (\"1\", \"b3\", \"4\", \"5\")),\n    (\"add9\", (\"1\", \"3\", \"5\", \"9\")),\n    (\"majadd9\", (\"1\", \"3\", \"5\", \"9\")),\n    (\"Madd9\", (\"1\", \"3\", \"5\", \"9\")),\n    (\"madd9\", (\"1\", \"b3\", \"5\", \"9\")),\n    (\"sus4add9\", (\"1\", \"4\", \"5\", \"9\")),\n    (\"sus4add2\", (\"1\", \"2\", \"4\", \"5\")),\n    (\"2\", (\"1\", \"3\", \"5\", \"9\")),\n    (\"add11\", (\"1\", \"3\", \"5\", \"11\")),\n    (\"4\", (\"1\", \"3\", \"5\", \"11\")),\n    # 5 notes\n    (\"m69\", (\"1\", \"b3\", \"5\", \"6\", \"9\")),\n    (\"69\", (\"1\", \"3\", \"5\", \"6\", \"9\")),\n    (\"9\", (\"1\", \"3\", \"5\", \"b7\", \"9\")),\n    (\"m9\", (\"1\", \"b3\", \"5\", \"b7\", \"9\")),\n    (\"M9\", (\"1\", \"3\", \"5\", \"7\", \"9\")),\n    (\"maj9\", (\"1\", \"3\", \"5\", \"7\", \"9\")),\n    (\"9sus4\", (\"1\", \"4\", \"5\", \"b7\", \"9\")),\n    (\"7-9\", (\"1\", \"3\", \"5\", \"b7\", \"b9\")),\n    (\"7b9\", (\"1\", \"3\", \"5\", \"b7\", \"b9\")),\n    # https://www.oolimo.com/guitarchords/Fsharp7(b9)\n    (\"7(b9)\", (\"1\", \"3\", \"5\", \"b7\", \"b9\")),\n    (\"7+9\", (\"1\", \"3\", \"5\", \"b7\", \"#9\")),\n    (\"7#9\", (\"1\", \"3\", \"5\", \"b7\", \"#9\")),\n    (\"9-5\", (\"1\", \"3\", \"b5\", \"b7\", \"9\")),\n    (\"9b5\", (\"1\", \"3\", \"b5\", \"b7\", \"9\")),\n    (\"9+5\", (\"1\", \"3\", \"#5\", \"b7\", \"9\")),\n    (\"9#5\", (\"1\", \"3\", \"#5\", \"b7\", \"9\")),\n    (\"7#9b5\", (\"1\", \"3\", \"b5\", \"b7\", \"#9\")),\n    (\"7#9#5\", (\"1\", \"3\", \"#5\", \"b7\", \"#9\")),\n    (\"m7b9b5\", (\"1\", \"b3\", \"b5\", \"b7\", \"b9\")),\n    (\"7b9b5\", (\"1\", \"3\", \"b5\", \"b7\", \"b9\")),\n    (\"7b9#5\", (\"1\", \"3\", \"#5\", \"b7\", \"b9\")),\n    (\"7+11\", (\"1\", \"3\", \"5\", \"b7\", \"#11\")),\n    (\"7#11\", (\"1\", \"3\", \"5\", \"b7\", \"#11\")),\n    (\"maj7+11\", (\"1\", \"3\", \"5\", \"7\", \"#11\")),\n    (\"M7+11\", (\"1\", \"3\", \"5\", \"7\", \"#11\")),\n    (\"maj7#11\", (\"1\", \"3\", \"5\", \"7\", \"#11\")),\n    (\"M7#11\", (\"1\", \"3\", \"5\", \"7\", \"#11\")),\n    (\"7-13\", (\"1\", \"3\", \"5\", \"b7\", \"b13\")),\n    (\"7b13\", (\"1\", \"3\", \"5\", \"b7\", \"b13\")),\n    (\"m7add11\", (\"1\", \"b3\", \"5\", \"b7\", \"11\")),\n    (\"maj7add11\", (\"1\", \"3\", \"5\", \"7\", \"11\")),\n    (\"M7add11\", (\"1\", \"3\", \"5\", \"7\", \"11\")),\n    (\"mmaj7add11\", (\"1\", \"b3\", \"5\", \"7\", \"11\")),\n    (\"mM7add11\", (\"1\", \"b3\", \"5\", \"7\", \"11\")),\n    (\"maj7add13\", (\"1\", \"3\", \"5\", \"7\", \"13\")),\n    (\"M7add13\", (\"1\", \"3\", \"5\", \"7\", \"13\")),\n    # 6 notes\n    (\"7b9#9\", (\"1\", \"3\", \"5\", \"b7\", \"b9\", \"#9\")),\n    (\"7b9#11\", (\"1\", \"3\", \"5\", \"b7\", \"b9\", \"#11\")),\n    (\"7#9#11\", (\"1\", \"3\", \"5\", \"b7\", \"#9\", \"#11\")),\n    (\"9+11\", (\"1\", \"3\", \"5\", \"b7\", \"9\", \"#11\")),\n    (\"9#11\", (\"1\", \"3\", \"5\", \"b7\", \"9\", \"#11\")),\n    (\"11\", (\"1\", \"3\", \"5\", \"b7\", \"9\", \"11\")),\n    # https://chord-c.com/guitar-chord/B/minor-eleventh/\n    (\"m11\", (\"1\", \"b3\", \"5\", \"b7\", \"9\", \"11\")),\n    # 7 notes\n    (\"7b9b13\", (\"1\", \"3\", \"5\", \"b7\", \"b9\", \"11\", \"b13\")),\n    (\"13\", (\"1\", \"3\", \"5\", \"b7\", \"9\", \"11\", \"13\")),\n    (\"13-9\", (\"1\", \"3\", \"5\", \"b7\", \"b9\", \"11\", \"13\")),\n    (\"13b9\", (\"1\", \"3\", \"5\", \"b7\", \"b9\", \"11\", \"13\")),\n    (\"13+9\", (\"1\", \"3\", \"5\", \"b7\", \"#9\", \"11\", \"13\")),\n    (\"13#9\", (\"1\", \"3\", \"5\", \"b7\", \"#9\", \"11\", \"13\")),\n    (\"13+11\", (\"1\", \"3\", \"5\", \"b7\", \"9\", \"#11\", \"13\")),\n    (\"13#11\", (\"1\", \"3\", \"5\", \"b7\", \"9\", \"#11\", \"13\")),\n    (\"maj13\", (\"1\", \"3\", \"5\", \"7\", \"9\", \"11\", \"13\")),\n    (\"M13\", (\"1\", \"3\", \"5\", \"7\", \"9\", \"11\", \"13\")),\n]\n"
  },
  {
    "path": "pychord/constants/scales.py",
    "content": "NOTE_VALUES = {\n    \"C\": 0,\n    \"D\": 2,\n    \"E\": 4,\n    \"F\": 5,\n    \"G\": 7,\n    \"A\": 9,\n    \"B\": 11,\n}\n\nSHARPED_SCALE = {\n    0: \"C\",\n    1: \"C#\",\n    2: \"D\",\n    3: \"D#\",\n    4: \"E\",\n    5: \"F\",\n    6: \"F#\",\n    7: \"G\",\n    8: \"G#\",\n    9: \"A\",\n    10: \"A#\",\n    11: \"B\",\n}\n\nFLATTED_SCALE = {\n    0: \"C\",\n    1: \"Db\",\n    2: \"D\",\n    3: \"Eb\",\n    4: \"E\",\n    5: \"F\",\n    6: \"Gb\",\n    7: \"G\",\n    8: \"Ab\",\n    9: \"A\",\n    10: \"Bb\",\n    11: \"B\",\n}\n\nSCALE_VAL_DICT = {\n    \"Ab\": FLATTED_SCALE,\n    \"A\": SHARPED_SCALE,\n    \"A#\": SHARPED_SCALE,\n    \"Bb\": FLATTED_SCALE,\n    \"B\": SHARPED_SCALE,\n    \"Cb\": FLATTED_SCALE,\n    \"C\": FLATTED_SCALE,\n    \"C#\": SHARPED_SCALE,\n    \"Db\": FLATTED_SCALE,\n    \"D\": SHARPED_SCALE,\n    \"D#\": SHARPED_SCALE,\n    \"Eb\": FLATTED_SCALE,\n    \"E\": SHARPED_SCALE,\n    \"F\": FLATTED_SCALE,\n    \"F#\": SHARPED_SCALE,\n    \"Gb\": FLATTED_SCALE,\n    \"G\": SHARPED_SCALE,\n    \"G#\": SHARPED_SCALE,\n}\n\n# https://en.wikipedia.org/wiki/Mode_(music)#Modern_modes\n# Ionian -> maj, Aeolian -> min\nRELATIVE_KEY_DICT = {\n    \"maj\": [0, 2, 4, 5, 7, 9, 11, 12],\n    \"Dor\": [0, 2, 3, 5, 7, 9, 10, 12],\n    \"Phr\": [0, 1, 3, 5, 7, 8, 10, 12],\n    \"Lyd\": [0, 2, 4, 6, 7, 9, 11, 12],\n    \"Mix\": [0, 2, 4, 5, 7, 9, 10, 12],\n    \"min\": [0, 2, 3, 5, 7, 8, 10, 12],\n    \"Loc\": [0, 1, 3, 5, 6, 8, 10, 12],\n}\n"
  },
  {
    "path": "pychord/parser.py",
    "content": "import re\n\nfrom .constants.scales import RELATIVE_KEY_DICT\nfrom .quality import QualityManager, Quality\n\n# We accept notes with up to two flats or two sharps.\nnote_re = re.compile(\"^[A-G](b{0,2}|#{0,2})$\")\n\ninversion_re = re.compile(\"/([0-9]+)\")\n\n\ndef _check_mode(mode: str) -> None:\n    \"\"\"Raise ValueError if mode is invalid\"\"\"\n    if mode not in RELATIVE_KEY_DICT:\n        raise ValueError(f\"Invalid mode {mode}\")\n\n\ndef _check_note(note: str) -> None:\n    \"\"\"Raise ValueError if note is invalid\"\"\"\n    if not note_re.match(note):\n        raise ValueError(f\"Invalid note {note}\")\n\n\ndef parse(chord: str) -> tuple[str, Quality, str]:\n    \"\"\"\n    Parse a string to get chord component.\n\n    :param chord: Name of the chord.\n    :return: (root, quality, on)\n    \"\"\"\n\n    if len(chord) > 2 and chord[1:3] in (\"bb\", \"##\"):\n        root = chord[:3]\n        rest = chord[3:]\n    elif len(chord) > 1 and chord[1] in (\"b\", \"#\"):\n        root = chord[:2]\n        rest = chord[2:]\n    else:\n        root = chord[:1]\n        rest = chord[1:]\n\n    _check_note(root)\n\n    inversion = 0\n    inversion_m = inversion_re.search(rest)\n    if inversion_m:\n        inversion = int(inversion_m.group(1))\n        rest = inversion_re.sub(\"\", rest)\n\n    on_chord_idx = rest.find(\"/\")\n    if on_chord_idx >= 0:\n        on = rest[on_chord_idx + 1 :]\n        rest = rest[:on_chord_idx]\n        _check_note(on)\n    else:\n        on = \"\"\n    quality = QualityManager().get_quality(rest, inversion)\n    return root, quality, on\n\n\ndef parse_scale(scale: str) -> tuple[str, str]:\n    \"\"\"\n    Parse a string representing a scale into its root and mode.\n    \"\"\"\n    root = scale[:-3]\n    mode = scale[-3:]\n\n    _check_note(root)\n    _check_mode(mode)\n\n    return (root, mode)\n"
  },
  {
    "path": "pychord/progression.py",
    "content": "from typing import Any\n\nfrom .chord import Chord\n\n\nclass ChordProgression:\n    \"\"\"\n    A chord progression, which is a sequence of :class:`Chord` instances.\n\n    :param initial_chords: Initial chord or chords of the chord progression.\n    \"\"\"\n\n    def __init__(\n        self, initial_chords: str | Chord | list[str] | list[Chord] = []\n    ) -> None:\n        if isinstance(initial_chords, Chord):\n            chords = [initial_chords]\n        elif isinstance(initial_chords, str):\n            chords = [self._as_chord(initial_chords)]\n        elif isinstance(initial_chords, list):\n            chords = [self._as_chord(chord) for chord in initial_chords]\n        else:\n            raise TypeError(\n                f\"Cannot initialize ChordProgression with argument of {type(initial_chords)} type\"\n            )\n        self._chords: list[Chord] = chords\n\n    def __str__(self) -> str:\n        return \" | \".join([chord.chord for chord in self._chords])\n\n    def __repr__(self) -> str:\n        return f\"<ChordProgression: {self}>\"\n\n    def __add__(self, other: \"ChordProgression\") -> \"ChordProgression\":\n        return ChordProgression(self._chords + other._chords)\n\n    def __len__(self) -> int:\n        return len(self._chords)\n\n    def __getitem__(self, key: int) -> Chord:\n        return self._chords[key]\n\n    def __setitem__(self, key: int, value: Chord) -> None:\n        self._chords[key] = value\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, ChordProgression):\n            raise TypeError(\n                f\"Cannot compare ChordProgression object with {type(other)} object\"\n            )\n        return self._chords == other._chords\n\n    @property\n    def chords(self) -> list[Chord]:\n        \"\"\"\n        The component chords of the chord progression.\n        \"\"\"\n        return self._chords\n\n    def append(self, chord: str | Chord) -> None:\n        \"\"\"\n        Append a chord to the chord progression.\n\n        :param chord: A chord to append.\n        \"\"\"\n        self._chords.append(self._as_chord(chord))\n\n    def insert(self, index: int, chord: str | Chord) -> None:\n        \"\"\"\n        Insert a chord into the chord progression.\n\n        :param index: Index to insert a chord.\n        :param chord: A chord to insert.\n        \"\"\"\n        self._chords.insert(index, self._as_chord(chord))\n\n    def pop(self, index: int = -1) -> Chord:\n        \"\"\"\n        Pop a chord from the chord progression.\n\n        :param index: Index of the chord to pop (default: -1).\n        \"\"\"\n        return self._chords.pop(index)\n\n    def transpose(self, trans: int) -> None:\n        \"\"\"\n        Transpose the whole chord progression.\n\n        :param trans: The number of semitones.\n        \"\"\"\n        for chord in self._chords:\n            chord.transpose(trans)\n\n    @staticmethod\n    def _as_chord(chord: str | Chord) -> Chord:\n        \"\"\"Convert from str to Chord instance if input is str.\n\n        :param chord: Chord name or :class:`Chord` instance.\n        \"\"\"\n        if isinstance(chord, Chord):\n            return chord\n        elif isinstance(chord, str):\n            return Chord(chord)\n        else:\n            raise TypeError(\"input type should be str or Chord instance.\")\n"
  },
  {
    "path": "pychord/py.typed",
    "content": "Marker\n"
  },
  {
    "path": "pychord/quality.py",
    "content": "import copy\nimport functools\nimport re\nfrom typing import Any, Literal, overload\n\nfrom .constants.qualities import DEFAULT_QUALITIES\nfrom .constants.scales import RELATIVE_KEY_DICT\nfrom .utils import augment, diminish, note_to_val\n\n\nclass Quality:\n    \"\"\"\n    A chord quality, defined by its intervals.\n\n    You should never need to create instances of this class yourself.\n\n    Use :class:`QualityManager` if you need to define a new quality or\n    override an existing one.\n\n    :param name: Name of the quality.\n    :param intervals: Intervals defining the quality.\n    \"\"\"\n\n    def __init__(self, name: str, intervals: tuple[str, ...]) -> None:\n        self._quality: str = name\n        self._intervals = intervals\n\n    def __str__(self) -> str:\n        return self._quality\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, Quality):\n            raise TypeError(f\"Cannot compare Quality object with {type(other)} object\")\n        return self.components == other.components\n\n    @property\n    def components(self) -> tuple[int, ...]:\n        return tuple(_get_interval_pitch(i) for i in self._intervals)\n\n    @property\n    def intervals(self) -> list[str]:\n        \"\"\"\n        The intervals definining the quality, e.g. ``[\"1\", \"3\", \"5\"]`` or ``[\"1\", \"b3\", \"5\", \"b7\"]``.\n        \"\"\"\n        return list(self._intervals)\n\n    @property\n    def quality(self) -> str:\n        \"\"\"\n        The name of the quality, e.g. ``\"maj\"``, ``\"m7\"``.\n        \"\"\"\n        return self._quality\n\n    @overload\n    def get_components(self, root: str, visible: Literal[True]) -> list[str]: ...\n\n    @overload\n    def get_components(self, root: str, visible: Literal[False]) -> list[int]: ...\n\n    @overload\n    def get_components(self, root: str, visible: bool) -> list[str] | list[int]: ...\n\n    def get_components(\n        self, root: str = \"C\", visible: bool = False\n    ) -> list[str] | list[int]:\n        \"\"\"Get components of chord quality\n\n        :param str root: the root note of the chord\n        :param bool visible: returns the name of notes if True\n        :rtype: list[str|int]\n        :return: components of chord quality\n        \"\"\"\n        if visible:\n            return [_apply_interval_to_note(root, i) for i in self._intervals]\n        else:\n            root_val = note_to_val(root)\n            return [v + root_val for v in self.components]\n\n\nclass QualityManager:\n    \"\"\"\n    Singleton class to manage the chord qualities.\n    \"\"\"\n\n    def __new__(cls) -> \"QualityManager\":\n        if not hasattr(cls, \"_instance\"):\n            cls._instance = super(QualityManager, cls).__new__(cls)\n            cls._instance.load_default_qualities()\n        return cls._instance\n\n    def load_default_qualities(self) -> None:\n        self._qualities = {q: Quality(q, c) for q, c in DEFAULT_QUALITIES}\n\n    def get_quality(self, name: str, inversion: int = 0) -> Quality:\n        if name not in self._qualities:\n            raise ValueError(f\"Unknown quality: {name}\")\n        # Create a new instance not to affect any existing instances\n        q = copy.deepcopy(self._qualities[name])\n        # apply requested inversion :\n        for i in range(inversion):\n            max_a, max_o = _parse_interval(q._intervals[-1])\n            a, o = _parse_interval(q._intervals[0])\n            while o < max_o:\n                o += 7\n            q._intervals = q._intervals[1:] + (f\"{a}{o + 1}\",)\n        return q\n\n    def get_qualities(self) -> dict[str, Quality]:\n        return dict(self._qualities)\n\n    def set_quality(self, name: str, intervals: tuple[str, ...]) -> None:\n        \"\"\"\n        Define a new quality or override an existing one.\n\n        This method will not affect any existing :class:`Chord` instances.\n\n        :param name: Name of the quality, e.g. ``\"m\"``.\n        :param intervals: Intervals defining the quality, e.g. ``[\"1\", \"b3\", \"5\"]``.\n        \"\"\"\n        self._qualities[name] = Quality(name, intervals)\n\n    def find_quality_from_components(self, components: list[int]) -> Quality | None:\n        \"\"\"\n        Find a quality from its components.\n\n        :param components: Components of the quality.\n        \"\"\"\n        for q in self._qualities.values():\n            if list(q.components) == components:\n                return copy.deepcopy(q)\n        return None\n\n\ndef _apply_interval_to_note(root: str, interval: str) -> str:\n    alterations, offset = _parse_interval(interval)\n\n    # Apply the interval and alteration.\n    notes_in_key = scale_notes(root, \"maj\")\n    note = notes_in_key[offset % 7]\n    for alteration in alterations:\n        if alteration == \"#\":\n            note = augment(note)\n        else:\n            note = diminish(note)\n    return note\n\n\ndef _get_interval_pitch(interval: str) -> int:\n    alterations, offset = _parse_interval(interval)\n\n    value = RELATIVE_KEY_DICT[\"maj\"][offset % 7] + 12 * (offset // 7)\n    for alteration in alterations:\n        if alteration == \"#\":\n            value += 1\n        else:\n            value -= 1\n    return value\n\n\ndef _parse_interval(interval: str) -> tuple[str, int]:\n    m = re.match(r\"^([b#]*)(\\d+)$\", interval)\n    assert m, f\"Invalid interval {interval}\"\n    alterations = m.group(1)\n    offset = int(m.group(2)) - 1\n    return alterations, offset\n\n\n@functools.lru_cache()\ndef scale_notes(root: str, mode: str) -> list[str]:\n    \"\"\"\n    Return the list of note names in the given scale.\n    \"\"\"\n    alphabet = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"]\n    root_val = note_to_val(root)\n\n    # Determine whether we use a flatted or sharped scale.\n    if root == \"F\" or len(root) > 1 and root[1] == \"b\":\n        alter = diminish\n    else:\n        alter = augment\n\n    # Name notes in the key.\n    notes = [root]\n    index = alphabet.index(root[0])\n    for offset in RELATIVE_KEY_DICT[mode][1:-1]:\n        index = (index + 1) % 7\n        note_val = (root_val + offset) % 12\n\n        # Find the accidental to match the pitch.\n        letter = alphabet[index]\n        for note in [\n            diminish(diminish(letter)),\n            diminish(letter),\n            letter,\n            augment(letter),\n            augment(augment(letter)),\n        ]:\n            if note_to_val(note) == note_val:\n                notes.append(note)\n                break\n            note = alter(note)\n        else:\n            raise ValueError(f\"{root}{mode} scale requires too many accidentals\")\n\n    return notes\n"
  },
  {
    "path": "pychord/utils.py",
    "content": "from .constants.scales import NOTE_VALUES, SCALE_VAL_DICT\n\n\ndef augment(note: str) -> str:\n    \"\"\"\n    Augment the given note.\n    \"\"\"\n    if note.endswith(\"b\"):\n        return note[:-1]\n    else:\n        return note + \"#\"\n\n\ndef diminish(note: str) -> str:\n    \"\"\"\n    Diminish the given note.\n    \"\"\"\n    if note.endswith(\"#\"):\n        return note[:-1]\n    else:\n        return note + \"b\"\n\n\ndef note_to_val(note: str) -> int:\n    \"\"\"Get index value of a note\n\n    >>> note_to_val(\"C\")\n    0\n    >>> note_to_val(\"B\")\n    11\n    \"\"\"\n    try:\n        pitch = NOTE_VALUES[note[0]]\n    except KeyError:\n        raise ValueError(f\"Unknown note {note}\")\n    for alteration in note[1:]:\n        if alteration == \"b\":\n            pitch -= 1\n        else:\n            pitch += 1\n    return pitch % 12\n\n\ndef transpose_note(note: str, transpose: int, scale: str = \"C\") -> str:\n    \"\"\"Transpose a note\n\n    >>> transpose_note(\"C\", 1)\n    \"Db\"\n    >>> transpose_note(\"D\", 4, \"A\")\n    \"F#\"\n    \"\"\"\n    val = note_to_val(note)\n    val += transpose\n    return SCALE_VAL_DICT[scale][val % 12]\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=45\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"pychord\"\nversion = \"1.3.2\"\ndescription = \"Package to handle musical chords\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nlicense = \"MIT\"\nauthors = [\n    {name = \"Yuma Mihira\", email = \"info@yuma.cloud\"},\n]\nkeywords = [\"music\", \"chord\"]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Console\",\n    \"Intended Audience :: Developers\",\n    \"Natural Language :: English\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3 :: Only\",\n    \"Topic :: Multimedia :: Sound/Audio\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Topic :: Utilities\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/yuma-m/pychord\"\n\n[project.optional-dependencies]\ntest = [\n    \"black>=26.1.0\",\n    \"mypy>=1.19.1\",\n]\n\n[tool.coverage.report]\nfail_under = 100\n\n[tool.coverage.run]\ninclude = [\"pychord/*\"]\n\n[tool.mypy]\nstrict = true\n\n[tool.setuptools]\ninclude-package-data = true\n\n[tool.setuptools.packages.find]\nwhere = [\".\"]\nexclude = [\"test\", \"docs\"]\n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nname = pychord\n\n[bdist_wheel]\nuniversal = 0\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\n\nif __name__ == \"__main__\":\n    setup()\n"
  },
  {
    "path": "test/__init__.py",
    "content": ""
  },
  {
    "path": "test/test_analyzer.py",
    "content": "import unittest\n\nfrom pychord import Chord\nfrom pychord.analyzer import (\n    get_all_rotated_notes,\n    find_chords_from_notes,\n    notes_to_positions,\n)\n\n\nclass TestNotesToPositions(unittest.TestCase):\n    def test_notes_to_positions(self):\n        for notes, root, expected_positions in [\n            ([\"C\"], \"C\", [0]),\n            ([\"C\", \"G\"], \"C\", [0, 7]),\n            ([\"D\", \"F#\", \"A\"], \"D\", [0, 4, 7]),\n            ([\"E\", \"G#\", \"B\", \"D\"], \"E\", [0, 4, 7, 10]),\n            ([\"Ab\", \"C\", \"Eb\", \"Bb\"], \"Ab\", [0, 4, 7, 14]),\n            ([\"F\", \"A\", \"C\", \"Eb\", \"G\"], \"F\", [0, 4, 7, 10, 14]),\n            ([\"G\", \"B\", \"D\", \"F\", \"A\", \"C\"], \"G\", [0, 4, 7, 10, 14, 17]),\n            ([\"A\", \"C#\", \"E\", \"G\", \"B\", \"D\", \"F#\"], \"A\", [0, 4, 7, 10, 14, 17, 21]),\n        ]:\n            with self.subTest(notes=notes, root=root):\n                self.assertEqual(notes_to_positions(notes, root), expected_positions)\n\n\nclass TestGetAllRotatedNotes(unittest.TestCase):\n    def test_get_all_rotated_notes(self):\n        for notes, expected_rotations in [\n            ([\"C\", \"G\"], [[\"C\", \"G\"], [\"G\", \"C\"]]),\n            ([\"C\", \"F\", \"G\"], [[\"C\", \"F\", \"G\"], [\"F\", \"G\", \"C\"], [\"G\", \"C\", \"F\"]]),\n        ]:\n            with self.subTest(notes=notes):\n                self.assertEqual(get_all_rotated_notes(notes), expected_rotations)\n\n\nclass TestFindChordsFromNotes(unittest.TestCase):\n    def test_empty(self):\n        with self.assertRaises(ValueError):\n            find_chords_from_notes([])\n\n    def test_find_chords_from_notes(self):\n        \"\"\"\n        Validates that the specified notes translated to the expected chords.\n        \"\"\"\n        for notes, expected_chord_strs in [\n            ([\"C\", \"E\", \"G\"], [\"C\"]),\n            ([\"F#\", \"A\", \"D\"], [\"D/F#\"]),\n            ([\"B\", \"E\", \"G#\"], [\"E/B\"]),\n            ([\"Eb\", \"Gb\", \"A\"], [\"Ebdim\"]),\n            ([\"G\", \"C\", \"D\"], [\"Gsus4\", \"Csus2/G\"]),\n            ([\"Eb\", \"Gb\", \"A\", \"C\"], [\"Ebdim7\", \"Gbdim7/Eb\", \"Adim7/Eb\", \"Cdim7/Eb\"]),\n            ([\"F\", \"A\", \"Db\"], [\"Faug\", \"Aaug/F\", \"Dbaug/F\"]),\n            ([\"C\", \"E\", \"G\", \"D\"], [\"Cadd9\"]),\n            ([\"F#\", \"A\", \"C\", \"E\"], [\"F#m7-5\", \"Am6/F#\", \"C6b5/F#\"]),\n            ([\"C\", \"E\", \"F\", \"G\"], [\"Cadd4\"]),\n            ([\"C\", \"Eb\", \"F\", \"G\"], [\"Cmadd4\"]),\n            ([\"C\", \"Eb\", \"G\", \"Bb\", \"F\"], [\"Cm7add11\"]),\n            ([\"C\", \"E\", \"G\", \"B\", \"F\"], [\"Cmaj7add11\"]),\n            ([\"C\", \"Eb\", \"G\", \"B\", \"F\"], [\"Cmmaj7add11\"]),\n            ([\"C\", \"E\", \"G\", \"B\", \"A\"], [\"Cmaj7add13\", \"Am9/C\"]),\n        ]:\n            with self.subTest(notes=notes):\n                chords = find_chords_from_notes(notes)\n                self.assertEqual([str(c) for c in chords], expected_chord_strs)\n\n    def test_idempotence(self):\n        for _ in range(2):\n            chords = find_chords_from_notes([\"Eb\", \"Gb\", \"Bbb\", \"Dbb\"])\n            self.assertEqual(\n                chords,\n                [\n                    Chord(\"Ebdim7\"),\n                    Chord(\"Gbdim7/Eb\"),\n                    Chord(\"Adim7/Eb\"),\n                    Chord(\"Cdim7/Eb\"),\n                ],\n            )\n            self.assertEqual(\n                chords[0].components(visible=True), [\"Eb\", \"Gb\", \"Bbb\", \"Dbb\"]\n            )\n"
  },
  {
    "path": "test/test_chord.py",
    "content": "import unittest\n\nfrom pychord import Chord\n\n\nclass TestChordCreations(unittest.TestCase):\n    def test_chord_creation(self):\n        for chord, expected_root, expected_quality in [\n            (\"C\", \"C\", \"\"),\n            (\"Am\", \"A\", \"m\"),\n            (\"A-\", \"A\", \"-\"),\n            (\"C69\", \"C\", \"69\"),\n            (\"Bm7-5\", \"B\", \"m7-5\"),\n            (\"Dm7b5\", \"D\", \"m7b5\"),\n        ]:\n            with self.subTest(chord=chord):\n                c = Chord(chord)\n                self.assertEqual(expected_root, c.root)\n                self.assertEqual(expected_quality, c.quality.quality)\n\n    def test_invalid_chord(self):\n        for chord in [\n            \"\",\n            \"Ab#\",  # mix of flat and sharp\n            \"A#b\",  # mix of flat and sharp\n            \"Abbb\",  # too many flats\n            \"A###\",  # too many sharps\n            \"H\",\n            \"Csus3\",\n            \"C/B###\",\n        ]:\n            with self.subTest(chord=chord):\n                self.assertRaises(ValueError, Chord, chord)\n\n    def test_slash_chord(self):\n        for chord, expected_root, expected_quality, expected_on in [\n            (\"F/G\", \"F\", \"\", \"G\"),\n            (\"Dm/G\", \"D\", \"m\", \"G\"),\n        ]:\n            with self.subTest(chord=chord):\n                c = Chord(chord)\n                self.assertEqual(expected_root, c.root)\n                self.assertEqual(expected_quality, c.quality.quality)\n                self.assertEqual(expected_on, c.on)\n\n    def test_invalid_slash_chord(self):\n        self.assertRaises(ValueError, Chord, \"C/H\")\n\n    def test_inversion(self):\n        for chord, expected_root, expected_quality, expected_components in [\n            (\"C/1\", \"C\", \"\", [\"E\", \"G\", \"C\"]),\n            (\"C/2\", \"C\", \"\", [\"G\", \"C\", \"E\"]),\n            (\"Dm7b5/1\", \"D\", \"m7b5\", [\"F\", \"Ab\", \"C\", \"D\"]),\n            (\"C/1/F\", \"C\", \"\", [\"F\", \"E\", \"G\", \"C\"]),\n        ]:\n            with self.subTest(chord=chord):\n                c = Chord(chord)\n                self.assertEqual(expected_root, c.root)\n                self.assertEqual(expected_quality, c.quality.quality)\n                self.assertEqual(expected_components, c.components())\n\n    def test_eq(self):\n        self.assertEqual(Chord(\"C\"), Chord(\"C\"))\n        self.assertEqual(Chord(\"C/G\"), Chord(\"C/G\"))\n\n    def test_eq_quality_alias(self):\n        self.assertEqual(Chord(\"Cmaj7\"), Chord(\"CM7\"))\n\n    def test_eq_root_alias(self):\n        self.assertEqual(Chord(\"C#\"), Chord(\"Db\"))\n\n    def test_eq_invalid(self):\n        with self.assertRaises(TypeError):\n            Chord(\"C\") == 0\n\n    def test_eq_different_root(self):\n        self.assertNotEqual(Chord(\"C\"), Chord(\"D\"))\n\n    def test_eq_different_quality(self):\n        self.assertNotEqual(Chord(\"C\"), Chord(\"Cm\"))\n\n    def test_eq_different_on(self):\n        self.assertNotEqual(Chord(\"C\"), Chord(\"C/G\"))\n        self.assertNotEqual(Chord(\"C/G\"), Chord(\"C\"))\n        self.assertNotEqual(Chord(\"C/B\"), Chord(\"C/G\"))\n\n    def test_components(self):\n        c = Chord(\"C/E\")\n        quality_components_before = c.quality.components\n        c.components()\n        self.assertEqual(c.quality.components, quality_components_before)\n\n    def test_info(self):\n        c = Chord(\"Cmaj7\")\n\n        # String representations.\n        self.assertEqual(repr(c), \"<Chord: Cmaj7>\")\n        self.assertEqual(str(c), \"Cmaj7\")\n\n        # Properties.\n        self.assertEqual(c.chord, \"Cmaj7\")\n        self.assertEqual(str(c.quality), \"maj7\")\n        self.assertEqual(c.root, \"C\")\n\n        # Methods.\n        self.assertEqual(\n            c.info(),\n            \"\"\"Cmaj7\nroot=C\nquality=maj7\non=\"\"\",\n        )\n\n\nclass TestChordFromNoteIndex(unittest.TestCase):\n    def test_from_note_index(self):\n        for note, quality, scale, expected_chord_str in [\n            (1, \"\", \"Cmaj\", \"C\"),\n            (2, \"m7\", \"F#min\", \"G#m7\"),\n            (3, \"sus2\", \"Cmin\", \"Ebsus2\"),\n            (7, \"7\", \"Amin\", \"G7\"),\n        ]:\n            with self.subTest(note=note, quality=quality, scale=scale):\n                chord = Chord.from_note_index(note=note, quality=quality, scale=scale)\n                self.assertEqual(str(chord), expected_chord_str)\n\n    def test_from_note_index_with_chromatic(self):\n        for note, quality, scale, chromatic, expected_chord_str in [\n            (1, \"\", \"Cmaj\", -1, \"Cb\"),\n            (1, \"\", \"Cmaj\", 1, \"C#\"),\n        ]:\n            with self.subTest(\n                note=note, quality=quality, scale=scale, chromatic=chromatic\n            ):\n                chord = Chord.from_note_index(\n                    note=note, quality=quality, scale=scale, chromatic=chromatic\n                )\n                self.assertEqual(str(chord), expected_chord_str)\n\n    def test_invalid_note_index(self):\n        for note, quality, scale, exception_str in [\n            (0, \"\", \"Cmaj\", \"Invalid note 0\"),\n            (8, \"\", \"Fmaj\", \"Invalid note 8\"),\n        ]:\n            with self.subTest(note=note, quality=quality, scale=scale):\n                with self.assertRaises(ValueError) as cm:\n                    Chord.from_note_index(note=note, quality=quality, scale=scale)\n                self.assertEqual(str(cm.exception), exception_str)\n\n    def test_invalid_scale(self):\n        for note, quality, scale, exception_str in [\n            (1, \"\", \"Xmaj\", \"Invalid note X\"),\n            (1, \"\", \"Cbob\", \"Invalid mode bob\"),\n        ]:\n            with self.subTest(note=note, quality=quality, scale=scale):\n                with self.assertRaises(ValueError) as cm:\n                    Chord.from_note_index(note=note, quality=quality, scale=scale)\n                self.assertEqual(str(cm.exception), exception_str)\n\n    def test_diatonic_from_note_index(self):\n        for note, quality, diatonic, scale, expected_chord_str in [\n            (1, \"\", True, \"Dmaj\", \"D\"),\n            (2, \"7\", True, \"BLoc\", \"CM7\"),\n            (3, \"m\", True, \"G#Mix\", \"B#dim\"),\n            (4, \"-\", True, \"AbDor\", \"Db\"),\n        ]:\n            with self.subTest(note=note, quality=quality, scale=scale):\n                chord = Chord.from_note_index(\n                    note=note, quality=quality, diatonic=diatonic, scale=scale\n                )\n                self.assertEqual(str(chord), expected_chord_str)\n\n    def test_diatonic_note_non_generic(self):\n        with self.assertRaises(NotImplementedError):\n            Chord.from_note_index(note=5, quality=\"sus\", diatonic=True, scale=\"Fmaj\")\n"
  },
  {
    "path": "test/test_component.py",
    "content": "import unittest\n\nfrom pychord import Chord\n\n\nclass TestChordComponent(unittest.TestCase):\n    def test_chord_components(self):\n        \"\"\"\n        Validates if a chord is made up of specified qualities and notes.\n        \"\"\"\n        for chord, expected_qualities, expected_notes in [\n            # Major chords with all supported accidentals.\n            (\"Abb\", [7, 11, 14], [\"Abb\", \"Cb\", \"Ebb\"]),\n            (\"Ab\", [8, 12, 15], [\"Ab\", \"C\", \"Eb\"]),\n            (\"A\", [9, 13, 16], [\"A\", \"C#\", \"E\"]),\n            (\"A#\", [10, 14, 17], [\"A#\", \"C##\", \"E#\"]),\n            (\"Bbb\", [9, 13, 16], [\"Bbb\", \"Db\", \"Fb\"]),\n            (\"Bb\", [10, 14, 17], [\"Bb\", \"D\", \"F\"]),\n            (\"B\", [11, 15, 18], [\"B\", \"D#\", \"F#\"]),\n            (\"B#\", [0, 4, 7], [\"B#\", \"D##\", \"F##\"]),\n            (\"Cbb\", [10, 14, 17], [\"Cbb\", \"Ebb\", \"Gbb\"]),\n            (\"Cb\", [11, 15, 18], [\"Cb\", \"Eb\", \"Gb\"]),\n            (\"C\", [0, 4, 7], [\"C\", \"E\", \"G\"]),\n            (\"C#\", [1, 5, 8], [\"C#\", \"E#\", \"G#\"]),\n            (\"C##\", [2, 6, 9], [\"C##\", \"E##\", \"G##\"]),\n            (\"Dbb\", [0, 4, 7], [\"Dbb\", \"Fb\", \"Abb\"]),\n            (\"Db\", [1, 5, 8], [\"Db\", \"F\", \"Ab\"]),\n            (\"D\", [2, 6, 9], [\"D\", \"F#\", \"A\"]),\n            (\"D#\", [3, 7, 10], [\"D#\", \"F##\", \"A#\"]),\n            (\"Ebb\", [2, 6, 9], [\"Ebb\", \"Gb\", \"Bbb\"]),\n            (\"Eb\", [3, 7, 10], [\"Eb\", \"G\", \"Bb\"]),\n            (\"E\", [4, 8, 11], [\"E\", \"G#\", \"B\"]),\n            (\"E#\", [5, 9, 12], [\"E#\", \"G##\", \"B#\"]),\n            (\"Fb\", [4, 8, 11], [\"Fb\", \"Ab\", \"Cb\"]),\n            (\"F\", [5, 9, 12], [\"F\", \"A\", \"C\"]),\n            (\"F#\", [6, 10, 13], [\"F#\", \"A#\", \"C#\"]),\n            (\"F##\", [7, 11, 14], [\"F##\", \"A##\", \"C##\"]),\n            (\"Gbb\", [5, 9, 12], [\"Gbb\", \"Bbb\", \"Dbb\"]),\n            (\"Gb\", [6, 10, 13], [\"Gb\", \"Bb\", \"Db\"]),\n            (\"G\", [7, 11, 14], [\"G\", \"B\", \"D\"]),\n            (\"G#\", [8, 12, 15], [\"G#\", \"B#\", \"D#\"]),\n            # Other chords.\n            (\"Am\", [9, 12, 16], [\"A\", \"C\", \"E\"]),\n            (\"Cbm\", [11, 14, 18], [\"Cb\", \"Ebb\", \"Gb\"]),\n            (\"Gm\", [7, 10, 14], [\"G\", \"Bb\", \"D\"]),\n            (\"Bdim\", [11, 14, 17], [\"B\", \"D\", \"F\"]),\n            (\"Cdim\", [0, 3, 6], [\"C\", \"Eb\", \"Gb\"]),\n            (\"Dbdim\", [1, 4, 7], [\"Db\", \"Fb\", \"Abb\"]),\n            (\"Ddim\", [2, 5, 8], [\"D\", \"F\", \"Ab\"]),\n            (\"Gbdim\", [6, 9, 12], [\"Gb\", \"Bbb\", \"Dbb\"]),\n            (\"Gdim\", [7, 10, 13], [\"G\", \"Bb\", \"Db\"]),\n            (\"Cdim7\", [0, 3, 6, 9], [\"C\", \"Eb\", \"Gb\", \"Bbb\"]),\n            (\"Dbdim7\", [1, 4, 7, 10], [\"Db\", \"Fb\", \"Abb\", \"Cbb\"]),\n            (\"Dbaug\", [1, 5, 9], [\"Db\", \"F\", \"A\"]),\n            (\"Eaug\", [4, 8, 12], [\"E\", \"G#\", \"B#\"]),\n            (\"CM9/D\", [-10, 0, 4, 7, 11], [\"D\", \"C\", \"E\", \"G\", \"B\"]),\n            (\"Fsus4\", [5, 10, 12], [\"F\", \"Bb\", \"C\"]),\n            (\"G7\", [7, 11, 14, 17], [\"G\", \"B\", \"D\", \"F\"]),\n            (\"G7b9\", [7, 11, 14, 17, 20], [\"G\", \"B\", \"D\", \"F\", \"Ab\"]),\n            (\"G7#11\", [7, 11, 14, 17, 25], [\"G\", \"B\", \"D\", \"F\", \"C#\"]),\n            (\"Gm7\", [7, 10, 14, 17], [\"G\", \"Bb\", \"D\", \"F\"]),\n            (\"C6\", [0, 4, 7, 9], [\"C\", \"E\", \"G\", \"A\"]),\n            (\"C#m7b9b5\", [1, 4, 7, 11, 14], [\"C#\", \"E\", \"G\", \"B\", \"D\"]),\n            (\"Db5\", [1, 8], [\"Db\", \"Ab\"]),\n            (\"D(b5)\", [2, 6, 8], [\"D\", \"F#\", \"Ab\"]),\n            (\"Cno5\", [0, 4], [\"C\", \"E\"]),\n            (\"Cadd4\", [0, 4, 5, 7], [\"C\", \"E\", \"F\", \"G\"]),\n            (\"CMadd4\", [0, 4, 5, 7], [\"C\", \"E\", \"F\", \"G\"]),\n            (\"Cmadd4\", [0, 3, 5, 7], [\"C\", \"Eb\", \"F\", \"G\"]),\n            (\"Csus4add9\", [0, 5, 7, 14], [\"C\", \"F\", \"G\", \"D\"]),\n            (\"Cm7add11\", [0, 3, 7, 10, 17], [\"C\", \"Eb\", \"G\", \"Bb\", \"F\"]),\n            (\"CM7add11\", [0, 4, 7, 11, 17], [\"C\", \"E\", \"G\", \"B\", \"F\"]),\n            (\"Dm7b5\", [2, 5, 8, 12], [\"D\", \"F\", \"Ab\", \"C\"]),\n            (\"Bm7-5\", [11, 14, 17, 21], [\"B\", \"D\", \"F\", \"A\"]),\n            (\"Ebm7b5\", [3, 6, 9, 13], [\"Eb\", \"Gb\", \"Bbb\", \"Db\"]),\n            (\"CmM7add11\", [0, 3, 7, 11, 17], [\"C\", \"Eb\", \"G\", \"B\", \"F\"]),\n            (\"CM7add13\", [0, 4, 7, 11, 21], [\"C\", \"E\", \"G\", \"B\", \"A\"]),\n            (\"C11\", [0, 4, 7, 10, 14, 17], [\"C\", \"E\", \"G\", \"Bb\", \"D\", \"F\"]),\n            (\"C13\", [0, 4, 7, 10, 14, 17, 21], [\"C\", \"E\", \"G\", \"Bb\", \"D\", \"F\", \"A\"]),\n        ]:\n            with self.subTest(chord=chord):\n                c = Chord(chord)\n                self.assertEqual(c.components(visible=False), expected_qualities)\n                self.assertEqual(c.components(visible=True), expected_notes)\n\n    def test_major_add9(self):\n        # major add 9 is a major chord with a Major ninth\n        base = Chord(\"C\")\n        base0 = list(base.components(visible=False))\n        base1 = list(base.components(visible=True))\n        c = Chord(\"CMadd9\")\n        com0 = c.components(visible=False)\n        self.assertEqual(com0, base0 + [14])\n        com1 = c.components(visible=True)\n        self.assertEqual(com1, base1 + [\"D\"])\n\n    def test_too_many_accidentals(self):\n        for chord in [\n            \"A##\",\n            \"B##\",\n            \"D##\",\n            \"E##\",\n            \"Fbb\",\n            \"G##\",\n        ]:\n            with self.subTest(chord=chord):\n                c = Chord(chord)\n                with self.assertRaises(ValueError) as cm:\n                    c.components()\n                self.assertEqual(\n                    str(cm.exception),\n                    f\"{chord}maj scale requires too many accidentals\",\n                )\n\n\nclass TestChordComponentWithPitch(unittest.TestCase):\n    def test_basic_chords_with_pitch(self):\n        \"\"\"\n        Validates if a chord with pitch is correctly calculated.\n        \"\"\"\n        for chord, root_pitch, expected in [\n            (\"C\", 1, [\"C1\", \"E1\", \"G1\"]),\n            (\"Am\", 2, [\"A2\", \"C3\", \"E3\"]),\n            (\"Dm7/G\", 3, [\"G3\", \"D4\", \"F4\", \"A4\", \"C5\"]),\n            (\"Eadd9\", 5, [\"E5\", \"G#5\", \"B5\", \"F#6\"]),\n        ]:\n            with self.subTest(chord=chord, root_pitch=root_pitch):\n                c = Chord(chord)\n                self.assertEqual(\n                    c.components_with_pitch(root_pitch=root_pitch), expected\n                )\n\n    def test_first_order_inversion(self):\n        c = Chord(\"G/1\")\n        com = c.components_with_pitch(root_pitch=4)\n        self.assertEqual(com, [\"B4\", \"D5\", \"G5\"])\n        c2 = Chord(\"G13b9/1\")\n        com2 = c2.components_with_pitch(root_pitch=4)\n        self.assertEqual(com2, [\"B4\", \"D5\", \"F5\", \"Ab5\", \"C6\", \"E6\", \"G6\"])\n\n    def test_second_order_inversion(self):\n        c = Chord(\"G/2\")\n        com = c.components_with_pitch(root_pitch=4)\n        self.assertEqual(com, [\"D5\", \"G5\", \"B5\"])\n        c2 = Chord(\"G13b9/2\")\n        com2 = c2.components_with_pitch(root_pitch=4)\n        self.assertEqual(com2, [\"D5\", \"F5\", \"Ab5\", \"C6\", \"E6\", \"G6\", \"B6\"])\n\n    def test_third_order_inversion(self):\n        c = Chord(\"Cm7/3\")\n        com = c.components_with_pitch(root_pitch=4)\n        self.assertEqual(com, [\"Bb4\", \"C5\", \"Eb5\", \"G5\"])\n        c2 = Chord(\"F#7/3\")\n        com2 = c2.components_with_pitch(root_pitch=4)\n        self.assertEqual(com2, [\"E5\", \"F#5\", \"A#5\", \"C#6\"])\n        c3 = Chord(\"G13b9/3\")\n        com3 = c3.components_with_pitch(root_pitch=4)\n        self.assertEqual(com3, [\"F5\", \"Ab5\", \"C6\", \"E6\", \"G6\", \"B6\", \"D7\"])\n\n    def test_fourth_order_inversion(self):\n        c = Chord(\"F7b9\")\n        com = c.components_with_pitch(root_pitch=4)\n        self.assertEqual(com, [\"F4\", \"A4\", \"C5\", \"Eb5\", \"Gb5\"])\n        c2 = Chord(\"G13b9/4\")\n        com2 = c2.components_with_pitch(root_pitch=4)\n        self.assertEqual(com2, [\"Ab5\", \"C6\", \"E6\", \"G6\", \"B6\", \"D7\", \"F7\"])\n\n    def test_fifth_order_inversion(self):\n        c = Chord(\"G13b9/5\")\n        com = c.components_with_pitch(root_pitch=4)\n        self.assertEqual(com, [\"C6\", \"E6\", \"G6\", \"B6\", \"D7\", \"F7\", \"Ab7\"])\n"
  },
  {
    "path": "test/test_progression.py",
    "content": "import unittest\n\nfrom pychord import Chord, ChordProgression\n\n\nclass TestChordProgressionCreations(unittest.TestCase):\n    def test_none(self):\n        cp = ChordProgression()\n        self.assertEqual(cp.chords, [])\n        self.assertEqual(str(cp), \"\")\n        self.assertEqual(repr(cp), \"<ChordProgression: >\")\n\n    def test_one_chord(self):\n        c = Chord(\"C\")\n        cp = ChordProgression(c)\n        self.assertEqual(cp.chords, [c])\n        self.assertEqual(str(cp), \"C\")\n        self.assertEqual(repr(cp), \"<ChordProgression: C>\")\n\n    def test_one_chord_str(self):\n        c = \"C\"\n        cp = ChordProgression(c)\n        self.assertEqual(cp.chords, [Chord(c)])\n        self.assertEqual(str(cp), \"C\")\n        self.assertEqual(repr(cp), \"<ChordProgression: C>\")\n\n    def test_one_chord_invalid_type(self):\n        with self.assertRaises(TypeError):\n            ChordProgression(1)\n\n    def test_one_chord_list(self):\n        c = Chord(\"C\")\n        cp = ChordProgression([c])\n        self.assertEqual(cp.chords, [c])\n\n    def test_one_chord_list_str(self):\n        c = \"C\"\n        cp = ChordProgression([c])\n        self.assertEqual(cp.chords, [Chord(c)])\n        self.assertEqual(str(cp), \"C\")\n        self.assertEqual(repr(cp), \"<ChordProgression: C>\")\n\n    def test_one_chord_list_invalid_type(self):\n        with self.assertRaises(TypeError):\n            ChordProgression([1])\n\n    def test_multiple_chords(self):\n        c1 = Chord(\"C\")\n        c2 = Chord(\"D\")\n        cp = ChordProgression([c1, c2])\n        self.assertEqual(cp.chords, [c1, c2])\n        self.assertEqual(str(cp), \"C | D\")\n        self.assertEqual(repr(cp), \"<ChordProgression: C | D>\")\n\n    def test_multiple_chords_str(self):\n        c1 = \"C\"\n        c2 = \"D\"\n        cp = ChordProgression([c1, c2])\n        self.assertEqual(cp.chords, [Chord(c1), Chord(c2)])\n        self.assertEqual(str(cp), \"C | D\")\n        self.assertEqual(repr(cp), \"<ChordProgression: C | D>\")\n\n\nclass TestChordProgressionFunctions(unittest.TestCase):\n    def test_append(self):\n        cp = ChordProgression([\"C\", \"D\", \"E\"])\n        cp.append(\"F\")\n        self.assertEqual(len(cp), 4)\n        self.assertEqual(cp.chords[-1], Chord(\"F\"))\n\n    def test_insert(self):\n        cp = ChordProgression([\"C\", \"D\", \"E\"])\n        cp.insert(0, \"F\")\n        self.assertEqual(len(cp), 4)\n        self.assertEqual(cp.chords[0], Chord(\"F\"))\n\n    def test_pop(self):\n        cp = ChordProgression([\"C\", \"D\", \"E\"])\n        c = cp.pop()\n        self.assertEqual(len(cp), 2)\n        self.assertEqual(c, Chord(\"E\"))\n\n    def test_transpose(self):\n        cp = ChordProgression([\"C\", \"F\", \"G\"])\n        cp.transpose(3)\n        self.assertEqual(cp.chords, [Chord(\"Eb\"), Chord(\"Ab\"), Chord(\"Bb\")])\n\n    def test_add(self):\n        cp1 = ChordProgression([\"C\", \"F\", \"G\"])\n        cp2 = ChordProgression([\"Am\", \"Em\"])\n        cp = cp1 + cp2\n        self.assertEqual(len(cp), 5)\n        self.assertEqual(\n            cp.chords, [Chord(\"C\"), Chord(\"F\"), Chord(\"G\"), Chord(\"Am\"), Chord(\"Em\")]\n        )\n\n        # Check the original progressions have not changed.\n        self.assertEqual(len(cp1), 3)\n        self.assertEqual(len(cp2), 2)\n\n    def test_self_add(self):\n        cp1 = ChordProgression([\"C\", \"F\", \"G\"])\n        cp2 = ChordProgression([\"Am\", \"Em\"])\n        cp1 += cp2\n        self.assertEqual(len(cp1), 5)\n        self.assertEqual(\n            cp1.chords, [Chord(\"C\"), Chord(\"F\"), Chord(\"G\"), Chord(\"Am\"), Chord(\"Em\")]\n        )\n\n    def test_get_item(self):\n        cp = ChordProgression([\"C\", \"F\", \"G\"])\n        self.assertEqual(cp[0], Chord(\"C\"))\n        self.assertEqual(cp[1], Chord(\"F\"))\n        self.assertEqual(cp[-1], Chord(\"G\"))\n\n    def test_set_item(self):\n        cp = ChordProgression([\"C\", \"F\", \"G\"])\n        cp[1] = Chord(\"E\")\n        self.assertEqual(cp[0], Chord(\"C\"))\n        self.assertEqual(cp[1], Chord(\"E\"))\n        self.assertEqual(cp[2], Chord(\"G\"))\n        self.assertEqual(len(cp), 3)\n\n    def test_slice(self):\n        cp = ChordProgression([\"C\", \"F\", \"G\"])\n        self.assertEqual(cp[0:1], [Chord(\"C\")])\n        self.assertEqual(cp[1:], [Chord(\"F\"), Chord(\"G\")])\n        self.assertEqual(cp[0::2], [Chord(\"C\"), Chord(\"G\")])\n\n    def test_eq(self):\n        cp1 = ChordProgression([\"C\", \"F\", \"G\"])\n        cp2 = ChordProgression([\"C\", \"F\", \"G\"])\n        self.assertEqual(cp1, cp2)\n        self.assertIsNot(cp1, cp2)\n\n    def test_invalid_eq(self):\n        cp = ChordProgression([\"C\", \"F\", \"G\"])\n        with self.assertRaises(TypeError):\n            print(cp == 0)\n"
  },
  {
    "path": "test/test_quality.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport unittest\n\nfrom pychord import QualityManager, Chord, find_chords_from_notes\n\n\nclass TestQuality(unittest.TestCase):\n    def setUp(self):\n        self.quality_manager = QualityManager()\n\n    def test_eq(self):\n        q1 = self.quality_manager.get_quality(\"m7-5\")\n        q2 = self.quality_manager.get_quality(\"m7-5\")\n        self.assertEqual(q1, q2)\n\n    def test_eq_alias_maj9(self):\n        q1 = self.quality_manager.get_quality(\"M9\")\n        q2 = self.quality_manager.get_quality(\"maj9\")\n        self.assertEqual(q1, q2)\n\n    def test_eq_alias_m7b5(self):\n        q1 = self.quality_manager.get_quality(\"m7-5\")\n        q2 = self.quality_manager.get_quality(\"m7b5\")\n        self.assertEqual(q1, q2)\n\n    def test_eq_alias_min(self):\n        q1 = self.quality_manager.get_quality(\"m\")\n        q2 = self.quality_manager.get_quality(\"min\")\n        q3 = self.quality_manager.get_quality(\"-\")\n        self.assertEqual(q1, q2)\n        self.assertEqual(q1, q3)\n\n    def test_invalid_eq(self):\n        q = self.quality_manager.get_quality(\"m7\")\n        with self.assertRaises(TypeError):\n            print(q == 0)\n\n    def subtest_quality_synonym(self, a, b):\n        with self.subTest(msg=f\"{a}_has_{b}_synonym\"):\n            a_quality = self.quality_manager.get_quality(a)\n            b_quality = self.quality_manager.get_quality(b)\n            self.assertEqual(a_quality, b_quality)\n\n    def test_maj_synonyms(self):\n        for q in self.quality_manager.get_qualities():\n            if q in [\"M\", \"maj\"]:\n                continue\n            if \"maj\" in q:\n                self.subtest_quality_synonym(q, q.replace(\"maj\", \"M\"))\n            elif \"M\" in q:\n                self.subtest_quality_synonym(q, q.replace(\"M\", \"maj\"))\n\n    def test_properties(self):\n        q = self.quality_manager.get_quality(\"m\")\n        self.assertEqual(str(q), \"m\")\n        self.assertEqual(q.intervals, [\"1\", \"b3\", \"5\"])\n        self.assertEqual(q.quality, \"m\")\n\n\nclass TestQualityManager(unittest.TestCase):\n    def test_singleton(self):\n        quality_manager = QualityManager()\n        quality_manager2 = QualityManager()\n        self.assertIs(quality_manager, quality_manager2)\n\n\nclass TestOverwriteQuality(unittest.TestCase):\n    def setUp(self):\n        self.quality_manager = QualityManager()\n\n    def tearDown(self):\n        self.quality_manager.load_default_qualities()\n\n    def test_overwrite(self):\n        # Remove the 9th from the \"11\" quality before building a chord.\n        self.quality_manager.set_quality(\"11\", (\"1\", \"3\", \"5\", \"b7\", \"11\"))\n        chord = Chord(\"C11\")\n        self.assertEqual(chord.components(), [\"C\", \"E\", \"G\", \"Bb\", \"F\"])\n\n    def test_find_from_components(self):\n        # Remove the 9th from the \"11\" quality then lookup a chord.\n        self.quality_manager.set_quality(\"11\", (\"1\", \"3\", \"5\", \"b7\", \"11\"))\n        chords = find_chords_from_notes([\"C\", \"E\", \"G\", \"Bb\", \"F\"])\n        self.assertEqual(chords, [Chord(\"C11\")])\n\n    def test_keep_existing_chord(self):\n        # Remove the 9th from the \"11\" quality after building a chord.\n        chord = Chord(\"C11\")\n        self.quality_manager.set_quality(\"11\", (\"1\", \"3\", \"5\", \"b7\", \"11\"))\n        self.assertEqual(chord.components(), [\"C\", \"E\", \"G\", \"Bb\", \"D\", \"F\"])\n\n\nclass TestIterateQualities(unittest.TestCase):\n    def setUp(self):\n        self.quality_manager = QualityManager()\n\n    def tearDown(self):\n        self.quality_manager.load_default_qualities()\n\n    def test_iterate_qualities(self):\n        assert \"m\" in self.quality_manager.get_qualities()\n\n    def test_immutable_qualities(self):\n        qualities = self.quality_manager.get_qualities()\n        assert \"testquality\" not in qualities\n        qualities[\"testquality\"] = qualities[\"m\"]\n        qualities = self.quality_manager.get_qualities()\n        assert \"testquality\" not in qualities\n\n    def test_iterate_added_qualities(self):\n        self.quality_manager.set_quality(\"testquality\", (\"1\",))\n        qualities = self.quality_manager.get_qualities()\n        assert \"testquality\" in qualities\n"
  },
  {
    "path": "test/test_transpose.py",
    "content": "import unittest\n\nfrom pychord import Chord\n\n\nclass TestChordTranspose(unittest.TestCase):\n    def test_transpose_zero(self):\n        c = Chord(\"Am\")\n        c.transpose(0)\n        self.assertEqual(c.root, \"A\")\n        self.assertEqual(c.quality.quality, \"m\")\n        self.assertEqual(c, Chord(\"Am\"))\n\n    def test_transpose_positive(self):\n        c = Chord(\"Am\")\n        c.transpose(3)\n        self.assertEqual(c.root, \"C\")\n        self.assertEqual(c.quality.quality, \"m\")\n        self.assertEqual(c, Chord(\"Cm\"))\n\n    def test_transpose_negative(self):\n        c = Chord(\"Am\")\n        c.transpose(-4)\n        self.assertEqual(c.root, \"F\")\n        self.assertEqual(c.quality.quality, \"m\")\n        self.assertEqual(c, Chord(\"Fm\"))\n\n    def test_transpose_slash(self):\n        c = Chord(\"Am7/G\")\n        c.transpose(3)\n        self.assertEqual(c.root, \"C\")\n        self.assertEqual(c.quality.quality, \"m7\")\n        self.assertEqual(c.on, \"Bb\")\n        self.assertEqual(c._chord, Chord(\"Cm7/Bb\")._chord)\n        self.assertEqual(c.quality.components, Chord(\"Cm7/Bb\").quality.components)\n        self.assertEqual(c, Chord(\"Cm7/Bb\"))\n\n    def test_transpose_inversion(self):\n        c = Chord(\"Am7/3\")\n        c.transpose(3)\n        self.assertEqual(c.root, \"C\")\n        self.assertEqual(c.quality.quality, \"m7\")\n\n    def test_invalid_transpose_type(self):\n        c = Chord(\"Am\")\n        self.assertRaises(TypeError, c.transpose, \"A\")\n\n    def test_transpose_eq1(self):\n        c = Chord(\"C\")\n        c.transpose(1)\n        self.assertEqual(c, Chord(\"C#\"))\n        self.assertEqual(c, Chord(\"Db\"))\n\n    def test_transpose_eq2(self):\n        c = Chord(\"C\")\n        c.transpose(2)\n        self.assertEqual(c, Chord(\"D\"))\n"
  },
  {
    "path": "test/test_utils.py",
    "content": "import unittest\n\nfrom pychord.utils import augment, diminish, note_to_val\n\n\nclass TestUtils(unittest.TestCase):\n    def test_augment(self):\n        self.assertEqual(augment(\"Cb\"), \"C\")\n        self.assertEqual(augment(\"C\"), \"C#\")\n        self.assertEqual(augment(\"C#\"), \"C##\")\n\n    def test_diminish(self):\n        self.assertEqual(diminish(\"Cb\"), \"Cbb\")\n        self.assertEqual(diminish(\"C\"), \"Cb\")\n        self.assertEqual(diminish(\"C#\"), \"C\")\n\n    def test_note_to_val(self):\n        self.assertEqual(note_to_val(\"C\"), 0)\n\n    def test_note_to_val_invalid(self):\n        with self.assertRaises(ValueError):\n            note_to_val(\"X\")\n"
  }
]