Full Code of yuma-m/pychord for AI

main 4bd676c14c24 cached
35 files
79.0 KB
25.0k tokens
168 symbols
1 requests
Download .txt
Repository: yuma-m/pychord
Branch: main
Commit: 4bd676c14c24
Files: 35
Total size: 79.0 KB

Directory structure:
gitextract_ch1k6q74/

├── .github/
│   └── workflows/
│       ├── build.yml
│       ├── codeql-analysis.yml
│       └── deploy.yml
├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── README.md
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   ├── pychord.rst
│   └── requirements.txt
├── examples/
│   └── pychord-midi.py
├── pychord/
│   ├── __init__.py
│   ├── analyzer.py
│   ├── chord.py
│   ├── constants/
│   │   ├── __init__.py
│   │   ├── qualities.py
│   │   └── scales.py
│   ├── parser.py
│   ├── progression.py
│   ├── py.typed
│   ├── quality.py
│   └── utils.py
├── pyproject.toml
├── setup.cfg
├── setup.py
└── test/
    ├── __init__.py
    ├── test_analyzer.py
    ├── test_chord.py
    ├── test_component.py
    ├── test_progression.py
    ├── test_quality.py
    ├── test_transpose.py
    └── test_utils.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/build.yml
================================================
name: Test and lint

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip setuptools wheel coverage[toml]
      - name: Run tests
        run: |
          coverage run -m unittest -v
          coverage report
      - name: Install linting tools
        run: |
          pip install black mypy
      - name: Run style check
        run: |
          black --check pychord test
      - name: Run type check
        run: |
          mypy pychord


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: CodeQL

on:
  push:
    branches:
      - main
  pull_request:
  schedule:
    - cron: '0 2 * * 1'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        language:
          - python
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
      - name: Autobuild
        uses: github/codeql-action/autobuild@v3
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3


================================================
FILE: .github/workflows/deploy.yml
================================================
name: Publish Python package

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install build twine
      - name: Build and publish
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        run: |
          python -m build
          twine upload dist/*


================================================
FILE: .gitignore
================================================
.idea/
venv/
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# 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/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/
make.bat

# PyBuilder
target/

#Ipython Notebook
.ipynb_checkpoints


================================================
FILE: .readthedocs.yaml
================================================
version: 2

build:
  os: ubuntu-lts-latest
  tools:
    python: "3.12"

sphinx:
  configuration: docs/conf.py

python:
  install:
    - requirements: docs/requirements.txt


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
![PyChord](https://github.com/yuma-m/pychord/raw/main/pychord.png)

# 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)

## Overview

PyChord is a Python library to handle musical chords.

## Installation

PyChord supports Python 3.10 and above.

```sh
$ pip install pychord
```

## Basic Usage

### Create a Chord

```python
>>> from pychord import Chord
>>> c = Chord("Am7")
>>> c
<Chord: Am7>
>>> c.info()
"""
Am7
root=A
quality=m7
on=None
"""
```

### Transpose a Chord

```python
>>> c = Chord("Am7/G")
>>> c.transpose(3)
>>> c
<Chord: Cm7/Bb>
```

### Get component notes

```python
>>> c = Chord("Am7")
>>> c.components()
['A', 'C', 'E', 'G']
>>> c.components_with_pitch(root_pitch=3)
['A3', 'C4', 'E4', 'G4']
```

### Compare Chords

```python
>>> Chord("C") == Chord("D")
False
>>> Chord("C#") == Chord("Db")
True
>>> c = Chord("C")
>>> c.transpose(2)
>>> c == Chord("D")
True
```

### Find Chords from notes

```python
>>> from pychord import find_chords_from_notes
>>> find_chords_from_notes(["C", "E", "G"])
[ <Chord: C>]
>>> find_chords_from_notes(["F#", "A", "C", "D"])
[ <Chord: D7/F#>]
>>> find_chords_from_notes(["F", "G", "C"])
[ <Chord: Fsus2>, <Chord: Csus4/F>]
```

### Create and handle chord progressions

```python
>>> from pychord import ChordProgression
>>> cp = ChordProgression(["C", "G/B", "Am"])
>>> cp
<ChordProgression: C | G/B | Am>

>>> cp.append("Em/G")
>>> cp
<ChordProgression: C | G/B | Am | Em/G>

>>> cp.transpose(+3)
>>> cp
<ChordProgression: Eb | Bb/D | Cm | Gm/Bb>

>>> cp[1]
<Chord: Bb/D>
```

## Advanced Usage

### Create a Chord from note index in a scale

```python
>>> Chord.from_note_index(note=1, quality="", scale="Cmaj")
<Chord: C>  # I of C major
>>> Chord.from_note_index(note=3, quality="m7", scale="Fmaj")
<Chord: Am7>  # IIIm7 of F major
>>> Chord.from_note_index(note=5, quality="7", scale="Amin")
<Chord: E7>  # V7 of A minor
```

### Overwrite the default Quality components with yours

```python
>>> from pychord import Chord, QualityManager
>>> Chord("C11").components()
['C', 'E', 'G', 'Bb', 'D', 'F']

>>> quality_manager = QualityManager()
>>> quality_manager.set_quality("11", ("1", "3", "5", "b7", "11"))
>>> Chord("C11").components()
['C', 'E', 'G', 'Bb', 'F']
```

### Inversions

Chord inversions are created with a forward slash and a number
indicating the order. This can optionally be combined with an
additional forward slash to change the bass note:

```python
>>> Chord("C/1").components() # First inversion of C
['E', 'G', 'C']
>>> Chord("C/2").components() # Second inversion of C
['G', 'C', 'E']

>>> Chord("Cm7/3/F").components() # Third inversion of Cm7 with an added F bass
['F', 'Bb', 'C', 'Eb', 'G']
```

## Examples

- [pychord-midi.py](./examples/pychord-midi.py) - Create a MIDI file using PyChord and pretty_midi.

## Links

- [PyPI](https://pypi.python.org/pypi/pychord)
- [GitHub](https://github.com/yuma-m/pychord)
- [Documentation](http://pychord.readthedocs.io/en/latest/)

## Author

- [Yuma Mihira](https://yuma.cloud/)

## License

- MIT License

The logo is made from [Freepik](https://www.flaticon.com/authors/freepik).


================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
PAPER         =
BUILDDIR      = _build

# Internal variables.
PAPEROPT_a4     = -D latex_elements.papersize=a4
PAPEROPT_letter = -D latex_elements.papersize=letter
ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

.PHONY: help
help:
	@echo "Please use \`make <target>' where <target> is one of"
	@echo "  html        to make standalone HTML files"
	@echo "  dirhtml     to make HTML files named index.html in directories"
	@echo "  singlehtml  to make a single large HTML file"
	@echo "  pickle      to make pickle files"
	@echo "  json        to make JSON files"
	@echo "  htmlhelp    to make HTML files and an HTML help project"
	@echo "  qthelp      to make HTML files and a qthelp project"
	@echo "  applehelp   to make an Apple Help Book"
	@echo "  devhelp     to make HTML files and a Devhelp project"
	@echo "  epub        to make an epub"
	@echo "  epub3       to make an epub3"
	@echo "  latex       to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
	@echo "  latexpdf    to make LaTeX files and run them through pdflatex"
	@echo "  latexpdfja  to make LaTeX files and run them through platex/dvipdfmx"
	@echo "  lualatexpdf to make LaTeX files and run them through lualatex"
	@echo "  xelatexpdf  to make LaTeX files and run them through xelatex"
	@echo "  text        to make text files"
	@echo "  man         to make manual pages"
	@echo "  texinfo     to make Texinfo files"
	@echo "  info        to make Texinfo files and run them through makeinfo"
	@echo "  gettext     to make PO message catalogs"
	@echo "  changes     to make an overview of all changed/added/deprecated items"
	@echo "  xml         to make Docutils-native XML files"
	@echo "  pseudoxml   to make pseudoxml-XML files for display purposes"
	@echo "  linkcheck   to check all external links for integrity"
	@echo "  doctest     to run all doctests embedded in the documentation (if enabled)"
	@echo "  coverage    to run coverage check of the documentation (if enabled)"
	@echo "  dummy       to check syntax errors of document sources"

.PHONY: clean
clean:
	rm -rf $(BUILDDIR)/*

.PHONY: html
html:
	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

.PHONY: dirhtml
dirhtml:
	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."

.PHONY: singlehtml
singlehtml:
	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
	@echo
	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."

.PHONY: pickle
pickle:
	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
	@echo
	@echo "Build finished; now you can process the pickle files."

.PHONY: json
json:
	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
	@echo
	@echo "Build finished; now you can process the JSON files."

.PHONY: htmlhelp
htmlhelp:
	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
	@echo
	@echo "Build finished; now you can run HTML Help Workshop with the" \
	      ".hhp project file in $(BUILDDIR)/htmlhelp."

.PHONY: qthelp
qthelp:
	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
	@echo
	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pychord.qhcp"
	@echo "To view the help file:"
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pychord.qhc"

.PHONY: applehelp
applehelp:
	$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
	@echo
	@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
	@echo "N.B. You won't be able to view it unless you put it in" \
	      "~/Library/Documentation/Help or install it in your application" \
	      "bundle."

.PHONY: devhelp
devhelp:
	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
	@echo
	@echo "Build finished."
	@echo "To view the help file:"
	@echo "# mkdir -p $$HOME/.local/share/devhelp/pychord"
	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pychord"
	@echo "# devhelp"

.PHONY: epub
epub:
	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
	@echo
	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."

.PHONY: epub3
epub3:
	$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
	@echo
	@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."

.PHONY: latex
latex:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo
	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
	@echo "Run \`make' in that directory to run these through (pdf)latex" \
	      "(use \`make latexpdf' here to do that automatically)."

.PHONY: latexpdf
latexpdf:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through pdflatex..."
	$(MAKE) -C $(BUILDDIR)/latex all-pdf
	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."

.PHONY: latexpdfja
latexpdfja:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through platex and dvipdfmx..."
	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."

.PHONY: lualatexpdf
lualatexpdf:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through lualatex..."
	$(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf
	@echo "lualatex finished; the PDF files are in $(BUILDDIR)/latex."

.PHONY: xelatexpdf
xelatexpdf:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through xelatex..."
	$(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf
	@echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex."

.PHONY: text
text:
	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
	@echo
	@echo "Build finished. The text files are in $(BUILDDIR)/text."

.PHONY: man
man:
	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
	@echo
	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."

.PHONY: texinfo
texinfo:
	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
	@echo
	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
	@echo "Run \`make' in that directory to run these through makeinfo" \
	      "(use \`make info' here to do that automatically)."

.PHONY: info
info:
	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
	@echo "Running Texinfo files through makeinfo..."
	make -C $(BUILDDIR)/texinfo info
	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."

.PHONY: gettext
gettext:
	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
	@echo
	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."

.PHONY: changes
changes:
	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
	@echo
	@echo "The overview file is in $(BUILDDIR)/changes."

.PHONY: linkcheck
linkcheck:
	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
	@echo
	@echo "Link check complete; look for any errors in the above output " \
	      "or in $(BUILDDIR)/linkcheck/output.txt."

.PHONY: doctest
doctest:
	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
	@echo "Testing of doctests in the sources finished, look at the " \
	      "results in $(BUILDDIR)/doctest/output.txt."

.PHONY: coverage
coverage:
	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
	@echo "Testing of coverage in the sources finished, look at the " \
	      "results in $(BUILDDIR)/coverage/python.txt."

.PHONY: xml
xml:
	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
	@echo
	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."

.PHONY: pseudoxml
pseudoxml:
	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
	@echo
	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

.PHONY: dummy
dummy:
	$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
	@echo
	@echo "Build finished. Dummy builder generates no files."


================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# pychord documentation build configuration file, created by
# sphinx-quickstart on Sat Dec 31 14:51:42 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# 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.join(os.path.dirname(os.path.abspath(__file__)), '..'))

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# 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.viewcode']

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = ['.rst', '.md']
# source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = u'pychord'
copyright = u'2016 - 2026, Yuma Mihira'
author = u'Yuma Mihira'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u''
# The full version, including alpha/beta/rc tags.
release = u''

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True


# -- 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 = 'sphinx_rtd_theme'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# 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']


# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'pychorddoc'


# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'pychord.tex', u'pychord Documentation',
     u'Author', 'manual'),
]


# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'pychord', u'pychord Documentation',
     [author], 1)
]


# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'pychord', u'pychord Documentation',
     author, 'pychord', 'One line description of project.',
     'Miscellaneous'),
]



# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']




================================================
FILE: docs/index.rst
================================================
.. pychord documentation master file, created by
   sphinx-quickstart on Sat Dec 31 14:51:42 2016.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

Welcome to pychord's documentation!
===================================

.. toctree::
   :maxdepth: 4
   :caption: Contents:

   pychord


Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`


================================================
FILE: docs/pychord.rst
================================================
pychord package
===============

.. automodule:: pychord
   :members:


================================================
FILE: docs/requirements.txt
================================================
sphinx==9.1.0
sphinx_rtd_theme==3.1.0


================================================
FILE: examples/pychord-midi.py
================================================
# An example to create MIDI file with PyChord and pretty_midi
# Prerequisite: pip install pretty_midi
# pretty_midi: https://github.com/craffel/pretty-midi


import pretty_midi

from pychord import Chord


def create_midi(chords):
    midi_data = pretty_midi.PrettyMIDI()
    piano_program = pretty_midi.instrument_name_to_program('Acoustic Grand Piano')
    piano = pretty_midi.Instrument(program=piano_program)
    length = 1
    for n, chord in enumerate(chords):
        for note_name in chord.components_with_pitch(root_pitch=4):
            note_number = pretty_midi.note_name_to_number(note_name)
            note = pretty_midi.Note(velocity=100, pitch=note_number, start=n * length, end=(n + 1) * length)
            piano.notes.append(note)
    midi_data.instruments.append(piano)
    midi_data.write('chord.mid')


def main():
    chords_str = ["C", "Dm7", "G", "C"]
    chords = [Chord(c) for c in chords_str]
    create_midi(chords)


if __name__ == '__main__':
    main()


================================================
FILE: pychord/__init__.py
================================================
from .analyzer import find_chords_from_notes
from .chord import Chord
from .progression import ChordProgression
from .quality import Quality, QualityManager

__all__ = [
    "find_chords_from_notes",
    "Chord",
    "ChordProgression",
    "Quality",
    "QualityManager",
]


================================================
FILE: pychord/analyzer.py
================================================
from .chord import Chord
from .quality import QualityManager
from .utils import note_to_val


def find_chords_from_notes(notes: list[str]) -> list[Chord]:
    """
    Find possible chords consisting of the given notes.

    :param notes: List of notes arranged from lower note, e.g. ``["C", "Eb", "G"]``.
    """
    if not notes:
        raise ValueError("Please specify notes which consist a chord.")
    root = notes[0]
    root_and_positions = []
    for rotated_notes in get_all_rotated_notes(notes):
        rotated_root = rotated_notes[0]
        root_and_positions.append(
            (rotated_root, notes_to_positions(rotated_notes, rotated_notes[0]))
        )
    chords = []
    for temp_root, positions in root_and_positions:
        quality = QualityManager().find_quality_from_components(positions)
        if quality is None:
            continue
        if temp_root == root:
            chord = "{}{}".format(root, quality)
        else:
            chord = "{}{}/{}".format(temp_root, quality, root)
        chords.append(Chord(chord))
    return chords


def notes_to_positions(notes: list[str], root: str) -> list[int]:
    """
    Get notes positions from the root note.

    >>> notes_to_positions(["C", "E", "G"], "C")
    [0, 4, 7]

    :param notes: List of notes.
    :param root: Root note.
    """
    root_pos = note_to_val(root)
    current_pos = root_pos
    positions = []
    for note in notes:
        note_pos = note_to_val(note)
        if note_pos < current_pos:
            note_pos += 12 * ((current_pos - note_pos) // 12 + 1)
        positions.append(note_pos - root_pos)
        current_pos = note_pos
    return positions


def get_all_rotated_notes(notes: list[str]) -> list[list[str]]:
    """
    Get all rotated notes.

    get_all_rotated_notes([A,C,E]) -> [[A,C,E],[C,E,A],[E,A,C]]
    """
    notes_list = []
    for x in range(len(notes)):
        notes_list.append(notes[x:] + notes[:x])
    return notes_list


================================================
FILE: pychord/chord.py
================================================
from typing import Any, Literal, overload

from .constants.scales import RELATIVE_KEY_DICT
from .parser import parse, parse_scale
from .quality import QualityManager, Quality, scale_notes
from .utils import augment, diminish, transpose_note, note_to_val


class Chord:
    """
    A chord, made up of two or more notes.

    :param chord: Name of the chord, e.g. ``"C"``, ``"Am7"``, ``"F#m7-5/A"``.
    """

    def __init__(self, chord: str) -> None:
        root, quality, on = parse(chord)
        self._chord: str = chord
        self._root: str = root
        self._quality: Quality = quality
        self._on: str = on

    def __str__(self) -> str:
        return self._chord

    def __repr__(self) -> str:
        return f"<Chord: {self._chord}>"

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, Chord):
            raise TypeError(f"Cannot compare Chord object with {type(other)} object")
        if note_to_val(self._root) != note_to_val(other.root):
            return False
        if self._quality != other.quality:
            return False

        if (
            # If one chord has an "on" and not the other, they differ.
            bool(self._on)
            != bool(other.on)
        ) or (
            # If both chords have an "on" and they are not enharmonic, they differ.
            self._on
            and other.on
            and note_to_val(self._on) != note_to_val(other.on)
        ):
            return False

        return True

    @classmethod
    def from_note_index(
        cls,
        note: int,
        quality: str,
        scale: str,
        diatonic: bool = False,
        chromatic: int = 0,
    ) -> "Chord":
        """Create a :class:`Chord` from a note index in a scale.

        - ``Chord.from_note_index(1, "", "Cmaj")`` returns I of C major => Chord("C")
        - ``Chord.from_note_index(3, "m7", "Fmaj")`` returns IIImin of F major => Chord("Am7")
        - ``Chord.from_note_index(5, "7", "Amin")`` returns Vmin of A minor => Chord("E7")
        - ``Chord.from_note_index(2, "", "Cmaj")`` returns II of C major => Chord("D")
        - ``Chord.from_note_index(2, "m", "Cmaj")`` returns IImin of C major => Chord("Dm")
        - ``Chord.from_note_index(2, "", "Cmaj", diatonic=True)`` returns IImin of C major => Chord("Dm")
        - ``Chord.from_note_index(2, "", "Cmin", chromatic=-1)`` returns bII of C minor => Chord("Db")

        :param note: Scale degree of the chord's root, ``1`` to ``7``.
        :param quality: Quality of the chord, e.g. ``"m7"``, ``"sus4"``.
        :param scale: Base scale, e.g. ``"Cmaj"``, ``"Amin"``, ``"F#maj"``, ``"Ebmin"``.
        :param diatonic: If True, chord quality is determined using the base scale (overrides ``quality``).
        :param chromatic: Lower or raise the scale degree (and all notes of the chord) by semitone(s).
        """
        if not 1 <= note <= 7:
            raise ValueError(f"Invalid note {note}")
        scale_root, scale_mode = parse_scale(scale)
        root = scale_notes(scale_root, scale_mode)[note - 1]
        if chromatic != 0:
            alter = augment if chromatic > 0 else diminish
            for i in range(abs(chromatic)):
                root = alter(root)

        if diatonic:
            scale_degrees = RELATIVE_KEY_DICT[scale_mode]

            # construct the chord based on scale degrees, within 1 octave
            first = scale_degrees[note - 1]
            third = scale_degrees[(note + 1) % 7]
            fifth = scale_degrees[(note + 3) % 7]
            seventh = scale_degrees[(note + 5) % 7]

            # adjust the chord to its root position (as a stack of thirds),
            # then set the root to 0
            # e.g. (9, 0, 4) -> [0, 3, 7]
            def get_diatonic_chord(chord: tuple[int, ...]) -> list[int]:
                uninverted: list[int] = []
                for note in chord:
                    if not uninverted:
                        uninverted.append(note)
                    elif note > uninverted[-1]:
                        uninverted.append(note)
                    else:
                        uninverted.append(note + 12)
                uninverted = [x - uninverted[0] for x in uninverted]
                return uninverted

            if quality in ["", "-", "maj", "m", "min"]:
                triad = (first, third, fifth)
                q = get_diatonic_chord(triad)
            elif quality in ["7", "M7", "maj7", "m7"]:
                seventh_chord = (first, third, fifth, seventh)
                q = get_diatonic_chord(seventh_chord)
            else:
                raise NotImplementedError(
                    "Only generic chords (triads, sevenths) are supported"
                )

            # look up QualityManager to determine chord quality
            quality_manager = QualityManager()
            quality_instance = quality_manager.find_quality_from_components(q)
            assert quality_instance is not None
            quality = quality_instance.quality

        return cls(f"{root}{quality}")

    @property
    def chord(self) -> str:
        """
        The name of the chord, e.g. ``"C"``, ``"Am7"``, ``"F#m7-5/A"``.
        """
        return self._chord

    @property
    def root(self) -> str:
        """
        The root note of the chord, e.g. ``"C"``, ``"A"``, ``"F#"``.
        """
        return self._root

    @property
    def quality(self) -> Quality:
        """
        The quality of the chord, e.g. ``"maj"``, ``"m7"``, ``"m7-5"``.
        """
        return self._quality

    @property
    def on(self) -> str:
        """
        The bass note of a slash chord.
        """
        return self._on

    def info(self) -> str:
        """
        Return information of chord to display.
        """
        return f"""{self._chord}
root={self._root}
quality={self._quality}
on={self._on}"""

    def transpose(self, trans: int, scale: str = "C") -> None:
        """
        Transpose the chord.

        :param trans: The number of semitones.
        :param scale: Key scale.
        """
        if not isinstance(trans, int):
            raise TypeError(f"Expected integers, not {type(trans)}")
        self._root = transpose_note(self._root, trans, scale)
        if self._on:
            self._on = transpose_note(self._on, trans, scale)
        self._reconfigure_chord()

    @overload
    def components(self, visible: Literal[True]) -> list[str]: ...

    @overload
    def components(self, visible: Literal[False]) -> list[int]: ...

    def components(self, visible: bool = True) -> list[str] | list[int]:
        """
        Return the component notes of the chord.

        :param visible: Returns the note names if ``True``, the note pitches otherwise.
        """
        if visible:
            notes = self._quality.get_components(root=self._root, visible=True)
            if self._on:
                notes = [n for n in notes if n != self._on]
                notes.insert(0, self._on)
            return notes
        else:
            components = self._quality.get_components(root=self._root, visible=False)
            if self._on:
                on_value = note_to_val(self._on)
                components = [c for c in components if c % 12 != on_value % 12]
                if on_value > components[0]:
                    on_value -= 12
                components.insert(0, on_value)
            return components

    def components_with_pitch(self, root_pitch: int) -> list[str]:
        """
        Return the component notes of chord formatted like ``["C4", "E4", "G4"]``.

        :param root_pitch: The pitch of the root note.
        """
        components = self.components(visible=False)
        notes = self.components(visible=True)
        if components[0] < 0:
            components = [c + 12 for c in components]
        return [f"{n}{root_pitch + c // 12}" for (n, c) in zip(notes, components)]

    def _reconfigure_chord(self) -> None:
        self._chord = "{}{}{}".format(
            self._root,
            self._quality.quality,
            f"/{self._on}" if self._on else "",
        )


================================================
FILE: pychord/constants/__init__.py
================================================


================================================
FILE: pychord/constants/qualities.py
================================================
# Do not import DEFAULT_QUALITIES directly
# Use QualityManager instead
DEFAULT_QUALITIES = [
    # chords consist of 2 notes
    ("5", ("1", "5")),
    ("no5", ("1", "3")),
    ("omit5", ("1", "3")),
    ("m(no5)", ("1", "b3")),
    ("m(omit5)", ("1", "b3")),
    # 3 notes
    ("", ("1", "3", "5")),
    ("maj", ("1", "3", "5")),
    ("m", ("1", "b3", "5")),
    ("min", ("1", "b3", "5")),
    ("-", ("1", "b3", "5")),
    ("dim", ("1", "b3", "b5")),
    # Not to confuse Ab5 with A(b5)
    ("(b5)", ("1", "3", "b5")),
    ("aug", ("1", "3", "#5")),
    ("sus2", ("1", "2", "5")),
    ("sus4", ("1", "4", "5")),
    ("sus", ("1", "4", "5")),
    # 4 notes
    ("6", ("1", "3", "5", "6")),
    # https://www.scales-chords.com/chord/piano/C%236b5
    ("6b5", ("1", "3", "b5", "6")),
    ("6-5", ("1", "3", "b5", "6")),
    ("7", ("1", "3", "5", "b7")),
    ("7-5", ("1", "3", "b5", "b7")),
    ("7b5", ("1", "3", "b5", "b7")),
    ("7+5", ("1", "3", "#5", "b7")),
    ("7#5", ("1", "3", "#5", "b7")),
    ("7sus4", ("1", "4", "5", "b7")),
    ("m6", ("1", "b3", "5", "6")),
    ("m7", ("1", "b3", "5", "b7")),
    ("m7-5", ("1", "b3", "b5", "b7")),
    ("m7b5", ("1", "b3", "b5", "b7")),
    ("m7+5", ("1", "b3", "#5", "b7")),
    ("m7#5", ("1", "b3", "#5", "b7")),
    ("dim7", ("1", "b3", "b5", "bb7")),
    ("M7", ("1", "3", "5", "7")),
    ("maj7", ("1", "3", "5", "7")),
    ("maj7+5", ("1", "3", "#5", "7")),
    ("M7+5", ("1", "3", "#5", "7")),
    ("mmaj7", ("1", "b3", "5", "7")),
    ("mM7", ("1", "b3", "5", "7")),
    ("add4", ("1", "3", "4", "5")),
    ("majadd4", ("1", "3", "4", "5")),
    ("Madd4", ("1", "3", "4", "5")),
    ("madd4", ("1", "b3", "4", "5")),
    ("add9", ("1", "3", "5", "9")),
    ("majadd9", ("1", "3", "5", "9")),
    ("Madd9", ("1", "3", "5", "9")),
    ("madd9", ("1", "b3", "5", "9")),
    ("sus4add9", ("1", "4", "5", "9")),
    ("sus4add2", ("1", "2", "4", "5")),
    ("2", ("1", "3", "5", "9")),
    ("add11", ("1", "3", "5", "11")),
    ("4", ("1", "3", "5", "11")),
    # 5 notes
    ("m69", ("1", "b3", "5", "6", "9")),
    ("69", ("1", "3", "5", "6", "9")),
    ("9", ("1", "3", "5", "b7", "9")),
    ("m9", ("1", "b3", "5", "b7", "9")),
    ("M9", ("1", "3", "5", "7", "9")),
    ("maj9", ("1", "3", "5", "7", "9")),
    ("9sus4", ("1", "4", "5", "b7", "9")),
    ("7-9", ("1", "3", "5", "b7", "b9")),
    ("7b9", ("1", "3", "5", "b7", "b9")),
    # https://www.oolimo.com/guitarchords/Fsharp7(b9)
    ("7(b9)", ("1", "3", "5", "b7", "b9")),
    ("7+9", ("1", "3", "5", "b7", "#9")),
    ("7#9", ("1", "3", "5", "b7", "#9")),
    ("9-5", ("1", "3", "b5", "b7", "9")),
    ("9b5", ("1", "3", "b5", "b7", "9")),
    ("9+5", ("1", "3", "#5", "b7", "9")),
    ("9#5", ("1", "3", "#5", "b7", "9")),
    ("7#9b5", ("1", "3", "b5", "b7", "#9")),
    ("7#9#5", ("1", "3", "#5", "b7", "#9")),
    ("m7b9b5", ("1", "b3", "b5", "b7", "b9")),
    ("7b9b5", ("1", "3", "b5", "b7", "b9")),
    ("7b9#5", ("1", "3", "#5", "b7", "b9")),
    ("7+11", ("1", "3", "5", "b7", "#11")),
    ("7#11", ("1", "3", "5", "b7", "#11")),
    ("maj7+11", ("1", "3", "5", "7", "#11")),
    ("M7+11", ("1", "3", "5", "7", "#11")),
    ("maj7#11", ("1", "3", "5", "7", "#11")),
    ("M7#11", ("1", "3", "5", "7", "#11")),
    ("7-13", ("1", "3", "5", "b7", "b13")),
    ("7b13", ("1", "3", "5", "b7", "b13")),
    ("m7add11", ("1", "b3", "5", "b7", "11")),
    ("maj7add11", ("1", "3", "5", "7", "11")),
    ("M7add11", ("1", "3", "5", "7", "11")),
    ("mmaj7add11", ("1", "b3", "5", "7", "11")),
    ("mM7add11", ("1", "b3", "5", "7", "11")),
    ("maj7add13", ("1", "3", "5", "7", "13")),
    ("M7add13", ("1", "3", "5", "7", "13")),
    # 6 notes
    ("7b9#9", ("1", "3", "5", "b7", "b9", "#9")),
    ("7b9#11", ("1", "3", "5", "b7", "b9", "#11")),
    ("7#9#11", ("1", "3", "5", "b7", "#9", "#11")),
    ("9+11", ("1", "3", "5", "b7", "9", "#11")),
    ("9#11", ("1", "3", "5", "b7", "9", "#11")),
    ("11", ("1", "3", "5", "b7", "9", "11")),
    # https://chord-c.com/guitar-chord/B/minor-eleventh/
    ("m11", ("1", "b3", "5", "b7", "9", "11")),
    # 7 notes
    ("7b9b13", ("1", "3", "5", "b7", "b9", "11", "b13")),
    ("13", ("1", "3", "5", "b7", "9", "11", "13")),
    ("13-9", ("1", "3", "5", "b7", "b9", "11", "13")),
    ("13b9", ("1", "3", "5", "b7", "b9", "11", "13")),
    ("13+9", ("1", "3", "5", "b7", "#9", "11", "13")),
    ("13#9", ("1", "3", "5", "b7", "#9", "11", "13")),
    ("13+11", ("1", "3", "5", "b7", "9", "#11", "13")),
    ("13#11", ("1", "3", "5", "b7", "9", "#11", "13")),
    ("maj13", ("1", "3", "5", "7", "9", "11", "13")),
    ("M13", ("1", "3", "5", "7", "9", "11", "13")),
]


================================================
FILE: pychord/constants/scales.py
================================================
NOTE_VALUES = {
    "C": 0,
    "D": 2,
    "E": 4,
    "F": 5,
    "G": 7,
    "A": 9,
    "B": 11,
}

SHARPED_SCALE = {
    0: "C",
    1: "C#",
    2: "D",
    3: "D#",
    4: "E",
    5: "F",
    6: "F#",
    7: "G",
    8: "G#",
    9: "A",
    10: "A#",
    11: "B",
}

FLATTED_SCALE = {
    0: "C",
    1: "Db",
    2: "D",
    3: "Eb",
    4: "E",
    5: "F",
    6: "Gb",
    7: "G",
    8: "Ab",
    9: "A",
    10: "Bb",
    11: "B",
}

SCALE_VAL_DICT = {
    "Ab": FLATTED_SCALE,
    "A": SHARPED_SCALE,
    "A#": SHARPED_SCALE,
    "Bb": FLATTED_SCALE,
    "B": SHARPED_SCALE,
    "Cb": FLATTED_SCALE,
    "C": FLATTED_SCALE,
    "C#": SHARPED_SCALE,
    "Db": FLATTED_SCALE,
    "D": SHARPED_SCALE,
    "D#": SHARPED_SCALE,
    "Eb": FLATTED_SCALE,
    "E": SHARPED_SCALE,
    "F": FLATTED_SCALE,
    "F#": SHARPED_SCALE,
    "Gb": FLATTED_SCALE,
    "G": SHARPED_SCALE,
    "G#": SHARPED_SCALE,
}

# https://en.wikipedia.org/wiki/Mode_(music)#Modern_modes
# Ionian -> maj, Aeolian -> min
RELATIVE_KEY_DICT = {
    "maj": [0, 2, 4, 5, 7, 9, 11, 12],
    "Dor": [0, 2, 3, 5, 7, 9, 10, 12],
    "Phr": [0, 1, 3, 5, 7, 8, 10, 12],
    "Lyd": [0, 2, 4, 6, 7, 9, 11, 12],
    "Mix": [0, 2, 4, 5, 7, 9, 10, 12],
    "min": [0, 2, 3, 5, 7, 8, 10, 12],
    "Loc": [0, 1, 3, 5, 6, 8, 10, 12],
}


================================================
FILE: pychord/parser.py
================================================
import re

from .constants.scales import RELATIVE_KEY_DICT
from .quality import QualityManager, Quality

# We accept notes with up to two flats or two sharps.
note_re = re.compile("^[A-G](b{0,2}|#{0,2})$")

inversion_re = re.compile("/([0-9]+)")


def _check_mode(mode: str) -> None:
    """Raise ValueError if mode is invalid"""
    if mode not in RELATIVE_KEY_DICT:
        raise ValueError(f"Invalid mode {mode}")


def _check_note(note: str) -> None:
    """Raise ValueError if note is invalid"""
    if not note_re.match(note):
        raise ValueError(f"Invalid note {note}")


def parse(chord: str) -> tuple[str, Quality, str]:
    """
    Parse a string to get chord component.

    :param chord: Name of the chord.
    :return: (root, quality, on)
    """

    if len(chord) > 2 and chord[1:3] in ("bb", "##"):
        root = chord[:3]
        rest = chord[3:]
    elif len(chord) > 1 and chord[1] in ("b", "#"):
        root = chord[:2]
        rest = chord[2:]
    else:
        root = chord[:1]
        rest = chord[1:]

    _check_note(root)

    inversion = 0
    inversion_m = inversion_re.search(rest)
    if inversion_m:
        inversion = int(inversion_m.group(1))
        rest = inversion_re.sub("", rest)

    on_chord_idx = rest.find("/")
    if on_chord_idx >= 0:
        on = rest[on_chord_idx + 1 :]
        rest = rest[:on_chord_idx]
        _check_note(on)
    else:
        on = ""
    quality = QualityManager().get_quality(rest, inversion)
    return root, quality, on


def parse_scale(scale: str) -> tuple[str, str]:
    """
    Parse a string representing a scale into its root and mode.
    """
    root = scale[:-3]
    mode = scale[-3:]

    _check_note(root)
    _check_mode(mode)

    return (root, mode)


================================================
FILE: pychord/progression.py
================================================
from typing import Any

from .chord import Chord


class ChordProgression:
    """
    A chord progression, which is a sequence of :class:`Chord` instances.

    :param initial_chords: Initial chord or chords of the chord progression.
    """

    def __init__(
        self, initial_chords: str | Chord | list[str] | list[Chord] = []
    ) -> None:
        if isinstance(initial_chords, Chord):
            chords = [initial_chords]
        elif isinstance(initial_chords, str):
            chords = [self._as_chord(initial_chords)]
        elif isinstance(initial_chords, list):
            chords = [self._as_chord(chord) for chord in initial_chords]
        else:
            raise TypeError(
                f"Cannot initialize ChordProgression with argument of {type(initial_chords)} type"
            )
        self._chords: list[Chord] = chords

    def __str__(self) -> str:
        return " | ".join([chord.chord for chord in self._chords])

    def __repr__(self) -> str:
        return f"<ChordProgression: {self}>"

    def __add__(self, other: "ChordProgression") -> "ChordProgression":
        return ChordProgression(self._chords + other._chords)

    def __len__(self) -> int:
        return len(self._chords)

    def __getitem__(self, key: int) -> Chord:
        return self._chords[key]

    def __setitem__(self, key: int, value: Chord) -> None:
        self._chords[key] = value

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, ChordProgression):
            raise TypeError(
                f"Cannot compare ChordProgression object with {type(other)} object"
            )
        return self._chords == other._chords

    @property
    def chords(self) -> list[Chord]:
        """
        The component chords of the chord progression.
        """
        return self._chords

    def append(self, chord: str | Chord) -> None:
        """
        Append a chord to the chord progression.

        :param chord: A chord to append.
        """
        self._chords.append(self._as_chord(chord))

    def insert(self, index: int, chord: str | Chord) -> None:
        """
        Insert a chord into the chord progression.

        :param index: Index to insert a chord.
        :param chord: A chord to insert.
        """
        self._chords.insert(index, self._as_chord(chord))

    def pop(self, index: int = -1) -> Chord:
        """
        Pop a chord from the chord progression.

        :param index: Index of the chord to pop (default: -1).
        """
        return self._chords.pop(index)

    def transpose(self, trans: int) -> None:
        """
        Transpose the whole chord progression.

        :param trans: The number of semitones.
        """
        for chord in self._chords:
            chord.transpose(trans)

    @staticmethod
    def _as_chord(chord: str | Chord) -> Chord:
        """Convert from str to Chord instance if input is str.

        :param chord: Chord name or :class:`Chord` instance.
        """
        if isinstance(chord, Chord):
            return chord
        elif isinstance(chord, str):
            return Chord(chord)
        else:
            raise TypeError("input type should be str or Chord instance.")


================================================
FILE: pychord/py.typed
================================================
Marker


================================================
FILE: pychord/quality.py
================================================
import copy
import functools
import re
from typing import Any, Literal, overload

from .constants.qualities import DEFAULT_QUALITIES
from .constants.scales import RELATIVE_KEY_DICT
from .utils import augment, diminish, note_to_val


class Quality:
    """
    A chord quality, defined by its intervals.

    You should never need to create instances of this class yourself.

    Use :class:`QualityManager` if you need to define a new quality or
    override an existing one.

    :param name: Name of the quality.
    :param intervals: Intervals defining the quality.
    """

    def __init__(self, name: str, intervals: tuple[str, ...]) -> None:
        self._quality: str = name
        self._intervals = intervals

    def __str__(self) -> str:
        return self._quality

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, Quality):
            raise TypeError(f"Cannot compare Quality object with {type(other)} object")
        return self.components == other.components

    @property
    def components(self) -> tuple[int, ...]:
        return tuple(_get_interval_pitch(i) for i in self._intervals)

    @property
    def intervals(self) -> list[str]:
        """
        The intervals definining the quality, e.g. ``["1", "3", "5"]`` or ``["1", "b3", "5", "b7"]``.
        """
        return list(self._intervals)

    @property
    def quality(self) -> str:
        """
        The name of the quality, e.g. ``"maj"``, ``"m7"``.
        """
        return self._quality

    @overload
    def get_components(self, root: str, visible: Literal[True]) -> list[str]: ...

    @overload
    def get_components(self, root: str, visible: Literal[False]) -> list[int]: ...

    @overload
    def get_components(self, root: str, visible: bool) -> list[str] | list[int]: ...

    def get_components(
        self, root: str = "C", visible: bool = False
    ) -> list[str] | list[int]:
        """Get components of chord quality

        :param str root: the root note of the chord
        :param bool visible: returns the name of notes if True
        :rtype: list[str|int]
        :return: components of chord quality
        """
        if visible:
            return [_apply_interval_to_note(root, i) for i in self._intervals]
        else:
            root_val = note_to_val(root)
            return [v + root_val for v in self.components]


class QualityManager:
    """
    Singleton class to manage the chord qualities.
    """

    def __new__(cls) -> "QualityManager":
        if not hasattr(cls, "_instance"):
            cls._instance = super(QualityManager, cls).__new__(cls)
            cls._instance.load_default_qualities()
        return cls._instance

    def load_default_qualities(self) -> None:
        self._qualities = {q: Quality(q, c) for q, c in DEFAULT_QUALITIES}

    def get_quality(self, name: str, inversion: int = 0) -> Quality:
        if name not in self._qualities:
            raise ValueError(f"Unknown quality: {name}")
        # Create a new instance not to affect any existing instances
        q = copy.deepcopy(self._qualities[name])
        # apply requested inversion :
        for i in range(inversion):
            max_a, max_o = _parse_interval(q._intervals[-1])
            a, o = _parse_interval(q._intervals[0])
            while o < max_o:
                o += 7
            q._intervals = q._intervals[1:] + (f"{a}{o + 1}",)
        return q

    def get_qualities(self) -> dict[str, Quality]:
        return dict(self._qualities)

    def set_quality(self, name: str, intervals: tuple[str, ...]) -> None:
        """
        Define a new quality or override an existing one.

        This method will not affect any existing :class:`Chord` instances.

        :param name: Name of the quality, e.g. ``"m"``.
        :param intervals: Intervals defining the quality, e.g. ``["1", "b3", "5"]``.
        """
        self._qualities[name] = Quality(name, intervals)

    def find_quality_from_components(self, components: list[int]) -> Quality | None:
        """
        Find a quality from its components.

        :param components: Components of the quality.
        """
        for q in self._qualities.values():
            if list(q.components) == components:
                return copy.deepcopy(q)
        return None


def _apply_interval_to_note(root: str, interval: str) -> str:
    alterations, offset = _parse_interval(interval)

    # Apply the interval and alteration.
    notes_in_key = scale_notes(root, "maj")
    note = notes_in_key[offset % 7]
    for alteration in alterations:
        if alteration == "#":
            note = augment(note)
        else:
            note = diminish(note)
    return note


def _get_interval_pitch(interval: str) -> int:
    alterations, offset = _parse_interval(interval)

    value = RELATIVE_KEY_DICT["maj"][offset % 7] + 12 * (offset // 7)
    for alteration in alterations:
        if alteration == "#":
            value += 1
        else:
            value -= 1
    return value


def _parse_interval(interval: str) -> tuple[str, int]:
    m = re.match(r"^([b#]*)(\d+)$", interval)
    assert m, f"Invalid interval {interval}"
    alterations = m.group(1)
    offset = int(m.group(2)) - 1
    return alterations, offset


@functools.lru_cache()
def scale_notes(root: str, mode: str) -> list[str]:
    """
    Return the list of note names in the given scale.
    """
    alphabet = ["C", "D", "E", "F", "G", "A", "B"]
    root_val = note_to_val(root)

    # Determine whether we use a flatted or sharped scale.
    if root == "F" or len(root) > 1 and root[1] == "b":
        alter = diminish
    else:
        alter = augment

    # Name notes in the key.
    notes = [root]
    index = alphabet.index(root[0])
    for offset in RELATIVE_KEY_DICT[mode][1:-1]:
        index = (index + 1) % 7
        note_val = (root_val + offset) % 12

        # Find the accidental to match the pitch.
        letter = alphabet[index]
        for note in [
            diminish(diminish(letter)),
            diminish(letter),
            letter,
            augment(letter),
            augment(augment(letter)),
        ]:
            if note_to_val(note) == note_val:
                notes.append(note)
                break
            note = alter(note)
        else:
            raise ValueError(f"{root}{mode} scale requires too many accidentals")

    return notes


================================================
FILE: pychord/utils.py
================================================
from .constants.scales import NOTE_VALUES, SCALE_VAL_DICT


def augment(note: str) -> str:
    """
    Augment the given note.
    """
    if note.endswith("b"):
        return note[:-1]
    else:
        return note + "#"


def diminish(note: str) -> str:
    """
    Diminish the given note.
    """
    if note.endswith("#"):
        return note[:-1]
    else:
        return note + "b"


def note_to_val(note: str) -> int:
    """Get index value of a note

    >>> note_to_val("C")
    0
    >>> note_to_val("B")
    11
    """
    try:
        pitch = NOTE_VALUES[note[0]]
    except KeyError:
        raise ValueError(f"Unknown note {note}")
    for alteration in note[1:]:
        if alteration == "b":
            pitch -= 1
        else:
            pitch += 1
    return pitch % 12


def transpose_note(note: str, transpose: int, scale: str = "C") -> str:
    """Transpose a note

    >>> transpose_note("C", 1)
    "Db"
    >>> transpose_note("D", 4, "A")
    "F#"
    """
    val = note_to_val(note)
    val += transpose
    return SCALE_VAL_DICT[scale][val % 12]


================================================
FILE: pyproject.toml
================================================
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "pychord"
version = "1.3.2"
description = "Package to handle musical chords"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
    {name = "Yuma Mihira", email = "info@yuma.cloud"},
]
keywords = ["music", "chord"]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Environment :: Console",
    "Intended Audience :: Developers",
    "Natural Language :: English",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3 :: Only",
    "Topic :: Multimedia :: Sound/Audio",
    "Topic :: Software Development :: Libraries :: Python Modules",
    "Topic :: Utilities",
]

[project.urls]
Homepage = "https://github.com/yuma-m/pychord"

[project.optional-dependencies]
test = [
    "black>=26.1.0",
    "mypy>=1.19.1",
]

[tool.coverage.report]
fail_under = 100

[tool.coverage.run]
include = ["pychord/*"]

[tool.mypy]
strict = true

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
where = ["."]
exclude = ["test", "docs"]


================================================
FILE: setup.cfg
================================================
[metadata]
name = pychord

[bdist_wheel]
universal = 0


================================================
FILE: setup.py
================================================
from setuptools import setup

if __name__ == "__main__":
    setup()


================================================
FILE: test/__init__.py
================================================


================================================
FILE: test/test_analyzer.py
================================================
import unittest

from pychord import Chord
from pychord.analyzer import (
    get_all_rotated_notes,
    find_chords_from_notes,
    notes_to_positions,
)


class TestNotesToPositions(unittest.TestCase):
    def test_notes_to_positions(self):
        for notes, root, expected_positions in [
            (["C"], "C", [0]),
            (["C", "G"], "C", [0, 7]),
            (["D", "F#", "A"], "D", [0, 4, 7]),
            (["E", "G#", "B", "D"], "E", [0, 4, 7, 10]),
            (["Ab", "C", "Eb", "Bb"], "Ab", [0, 4, 7, 14]),
            (["F", "A", "C", "Eb", "G"], "F", [0, 4, 7, 10, 14]),
            (["G", "B", "D", "F", "A", "C"], "G", [0, 4, 7, 10, 14, 17]),
            (["A", "C#", "E", "G", "B", "D", "F#"], "A", [0, 4, 7, 10, 14, 17, 21]),
        ]:
            with self.subTest(notes=notes, root=root):
                self.assertEqual(notes_to_positions(notes, root), expected_positions)


class TestGetAllRotatedNotes(unittest.TestCase):
    def test_get_all_rotated_notes(self):
        for notes, expected_rotations in [
            (["C", "G"], [["C", "G"], ["G", "C"]]),
            (["C", "F", "G"], [["C", "F", "G"], ["F", "G", "C"], ["G", "C", "F"]]),
        ]:
            with self.subTest(notes=notes):
                self.assertEqual(get_all_rotated_notes(notes), expected_rotations)


class TestFindChordsFromNotes(unittest.TestCase):
    def test_empty(self):
        with self.assertRaises(ValueError):
            find_chords_from_notes([])

    def test_find_chords_from_notes(self):
        """
        Validates that the specified notes translated to the expected chords.
        """
        for notes, expected_chord_strs in [
            (["C", "E", "G"], ["C"]),
            (["F#", "A", "D"], ["D/F#"]),
            (["B", "E", "G#"], ["E/B"]),
            (["Eb", "Gb", "A"], ["Ebdim"]),
            (["G", "C", "D"], ["Gsus4", "Csus2/G"]),
            (["Eb", "Gb", "A", "C"], ["Ebdim7", "Gbdim7/Eb", "Adim7/Eb", "Cdim7/Eb"]),
            (["F", "A", "Db"], ["Faug", "Aaug/F", "Dbaug/F"]),
            (["C", "E", "G", "D"], ["Cadd9"]),
            (["F#", "A", "C", "E"], ["F#m7-5", "Am6/F#", "C6b5/F#"]),
            (["C", "E", "F", "G"], ["Cadd4"]),
            (["C", "Eb", "F", "G"], ["Cmadd4"]),
            (["C", "Eb", "G", "Bb", "F"], ["Cm7add11"]),
            (["C", "E", "G", "B", "F"], ["Cmaj7add11"]),
            (["C", "Eb", "G", "B", "F"], ["Cmmaj7add11"]),
            (["C", "E", "G", "B", "A"], ["Cmaj7add13", "Am9/C"]),
        ]:
            with self.subTest(notes=notes):
                chords = find_chords_from_notes(notes)
                self.assertEqual([str(c) for c in chords], expected_chord_strs)

    def test_idempotence(self):
        for _ in range(2):
            chords = find_chords_from_notes(["Eb", "Gb", "Bbb", "Dbb"])
            self.assertEqual(
                chords,
                [
                    Chord("Ebdim7"),
                    Chord("Gbdim7/Eb"),
                    Chord("Adim7/Eb"),
                    Chord("Cdim7/Eb"),
                ],
            )
            self.assertEqual(
                chords[0].components(visible=True), ["Eb", "Gb", "Bbb", "Dbb"]
            )


================================================
FILE: test/test_chord.py
================================================
import unittest

from pychord import Chord


class TestChordCreations(unittest.TestCase):
    def test_chord_creation(self):
        for chord, expected_root, expected_quality in [
            ("C", "C", ""),
            ("Am", "A", "m"),
            ("A-", "A", "-"),
            ("C69", "C", "69"),
            ("Bm7-5", "B", "m7-5"),
            ("Dm7b5", "D", "m7b5"),
        ]:
            with self.subTest(chord=chord):
                c = Chord(chord)
                self.assertEqual(expected_root, c.root)
                self.assertEqual(expected_quality, c.quality.quality)

    def test_invalid_chord(self):
        for chord in [
            "",
            "Ab#",  # mix of flat and sharp
            "A#b",  # mix of flat and sharp
            "Abbb",  # too many flats
            "A###",  # too many sharps
            "H",
            "Csus3",
            "C/B###",
        ]:
            with self.subTest(chord=chord):
                self.assertRaises(ValueError, Chord, chord)

    def test_slash_chord(self):
        for chord, expected_root, expected_quality, expected_on in [
            ("F/G", "F", "", "G"),
            ("Dm/G", "D", "m", "G"),
        ]:
            with self.subTest(chord=chord):
                c = Chord(chord)
                self.assertEqual(expected_root, c.root)
                self.assertEqual(expected_quality, c.quality.quality)
                self.assertEqual(expected_on, c.on)

    def test_invalid_slash_chord(self):
        self.assertRaises(ValueError, Chord, "C/H")

    def test_inversion(self):
        for chord, expected_root, expected_quality, expected_components in [
            ("C/1", "C", "", ["E", "G", "C"]),
            ("C/2", "C", "", ["G", "C", "E"]),
            ("Dm7b5/1", "D", "m7b5", ["F", "Ab", "C", "D"]),
            ("C/1/F", "C", "", ["F", "E", "G", "C"]),
        ]:
            with self.subTest(chord=chord):
                c = Chord(chord)
                self.assertEqual(expected_root, c.root)
                self.assertEqual(expected_quality, c.quality.quality)
                self.assertEqual(expected_components, c.components())

    def test_eq(self):
        self.assertEqual(Chord("C"), Chord("C"))
        self.assertEqual(Chord("C/G"), Chord("C/G"))

    def test_eq_quality_alias(self):
        self.assertEqual(Chord("Cmaj7"), Chord("CM7"))

    def test_eq_root_alias(self):
        self.assertEqual(Chord("C#"), Chord("Db"))

    def test_eq_invalid(self):
        with self.assertRaises(TypeError):
            Chord("C") == 0

    def test_eq_different_root(self):
        self.assertNotEqual(Chord("C"), Chord("D"))

    def test_eq_different_quality(self):
        self.assertNotEqual(Chord("C"), Chord("Cm"))

    def test_eq_different_on(self):
        self.assertNotEqual(Chord("C"), Chord("C/G"))
        self.assertNotEqual(Chord("C/G"), Chord("C"))
        self.assertNotEqual(Chord("C/B"), Chord("C/G"))

    def test_components(self):
        c = Chord("C/E")
        quality_components_before = c.quality.components
        c.components()
        self.assertEqual(c.quality.components, quality_components_before)

    def test_info(self):
        c = Chord("Cmaj7")

        # String representations.
        self.assertEqual(repr(c), "<Chord: Cmaj7>")
        self.assertEqual(str(c), "Cmaj7")

        # Properties.
        self.assertEqual(c.chord, "Cmaj7")
        self.assertEqual(str(c.quality), "maj7")
        self.assertEqual(c.root, "C")

        # Methods.
        self.assertEqual(
            c.info(),
            """Cmaj7
root=C
quality=maj7
on=""",
        )


class TestChordFromNoteIndex(unittest.TestCase):
    def test_from_note_index(self):
        for note, quality, scale, expected_chord_str in [
            (1, "", "Cmaj", "C"),
            (2, "m7", "F#min", "G#m7"),
            (3, "sus2", "Cmin", "Ebsus2"),
            (7, "7", "Amin", "G7"),
        ]:
            with self.subTest(note=note, quality=quality, scale=scale):
                chord = Chord.from_note_index(note=note, quality=quality, scale=scale)
                self.assertEqual(str(chord), expected_chord_str)

    def test_from_note_index_with_chromatic(self):
        for note, quality, scale, chromatic, expected_chord_str in [
            (1, "", "Cmaj", -1, "Cb"),
            (1, "", "Cmaj", 1, "C#"),
        ]:
            with self.subTest(
                note=note, quality=quality, scale=scale, chromatic=chromatic
            ):
                chord = Chord.from_note_index(
                    note=note, quality=quality, scale=scale, chromatic=chromatic
                )
                self.assertEqual(str(chord), expected_chord_str)

    def test_invalid_note_index(self):
        for note, quality, scale, exception_str in [
            (0, "", "Cmaj", "Invalid note 0"),
            (8, "", "Fmaj", "Invalid note 8"),
        ]:
            with self.subTest(note=note, quality=quality, scale=scale):
                with self.assertRaises(ValueError) as cm:
                    Chord.from_note_index(note=note, quality=quality, scale=scale)
                self.assertEqual(str(cm.exception), exception_str)

    def test_invalid_scale(self):
        for note, quality, scale, exception_str in [
            (1, "", "Xmaj", "Invalid note X"),
            (1, "", "Cbob", "Invalid mode bob"),
        ]:
            with self.subTest(note=note, quality=quality, scale=scale):
                with self.assertRaises(ValueError) as cm:
                    Chord.from_note_index(note=note, quality=quality, scale=scale)
                self.assertEqual(str(cm.exception), exception_str)

    def test_diatonic_from_note_index(self):
        for note, quality, diatonic, scale, expected_chord_str in [
            (1, "", True, "Dmaj", "D"),
            (2, "7", True, "BLoc", "CM7"),
            (3, "m", True, "G#Mix", "B#dim"),
            (4, "-", True, "AbDor", "Db"),
        ]:
            with self.subTest(note=note, quality=quality, scale=scale):
                chord = Chord.from_note_index(
                    note=note, quality=quality, diatonic=diatonic, scale=scale
                )
                self.assertEqual(str(chord), expected_chord_str)

    def test_diatonic_note_non_generic(self):
        with self.assertRaises(NotImplementedError):
            Chord.from_note_index(note=5, quality="sus", diatonic=True, scale="Fmaj")


================================================
FILE: test/test_component.py
================================================
import unittest

from pychord import Chord


class TestChordComponent(unittest.TestCase):
    def test_chord_components(self):
        """
        Validates if a chord is made up of specified qualities and notes.
        """
        for chord, expected_qualities, expected_notes in [
            # Major chords with all supported accidentals.
            ("Abb", [7, 11, 14], ["Abb", "Cb", "Ebb"]),
            ("Ab", [8, 12, 15], ["Ab", "C", "Eb"]),
            ("A", [9, 13, 16], ["A", "C#", "E"]),
            ("A#", [10, 14, 17], ["A#", "C##", "E#"]),
            ("Bbb", [9, 13, 16], ["Bbb", "Db", "Fb"]),
            ("Bb", [10, 14, 17], ["Bb", "D", "F"]),
            ("B", [11, 15, 18], ["B", "D#", "F#"]),
            ("B#", [0, 4, 7], ["B#", "D##", "F##"]),
            ("Cbb", [10, 14, 17], ["Cbb", "Ebb", "Gbb"]),
            ("Cb", [11, 15, 18], ["Cb", "Eb", "Gb"]),
            ("C", [0, 4, 7], ["C", "E", "G"]),
            ("C#", [1, 5, 8], ["C#", "E#", "G#"]),
            ("C##", [2, 6, 9], ["C##", "E##", "G##"]),
            ("Dbb", [0, 4, 7], ["Dbb", "Fb", "Abb"]),
            ("Db", [1, 5, 8], ["Db", "F", "Ab"]),
            ("D", [2, 6, 9], ["D", "F#", "A"]),
            ("D#", [3, 7, 10], ["D#", "F##", "A#"]),
            ("Ebb", [2, 6, 9], ["Ebb", "Gb", "Bbb"]),
            ("Eb", [3, 7, 10], ["Eb", "G", "Bb"]),
            ("E", [4, 8, 11], ["E", "G#", "B"]),
            ("E#", [5, 9, 12], ["E#", "G##", "B#"]),
            ("Fb", [4, 8, 11], ["Fb", "Ab", "Cb"]),
            ("F", [5, 9, 12], ["F", "A", "C"]),
            ("F#", [6, 10, 13], ["F#", "A#", "C#"]),
            ("F##", [7, 11, 14], ["F##", "A##", "C##"]),
            ("Gbb", [5, 9, 12], ["Gbb", "Bbb", "Dbb"]),
            ("Gb", [6, 10, 13], ["Gb", "Bb", "Db"]),
            ("G", [7, 11, 14], ["G", "B", "D"]),
            ("G#", [8, 12, 15], ["G#", "B#", "D#"]),
            # Other chords.
            ("Am", [9, 12, 16], ["A", "C", "E"]),
            ("Cbm", [11, 14, 18], ["Cb", "Ebb", "Gb"]),
            ("Gm", [7, 10, 14], ["G", "Bb", "D"]),
            ("Bdim", [11, 14, 17], ["B", "D", "F"]),
            ("Cdim", [0, 3, 6], ["C", "Eb", "Gb"]),
            ("Dbdim", [1, 4, 7], ["Db", "Fb", "Abb"]),
            ("Ddim", [2, 5, 8], ["D", "F", "Ab"]),
            ("Gbdim", [6, 9, 12], ["Gb", "Bbb", "Dbb"]),
            ("Gdim", [7, 10, 13], ["G", "Bb", "Db"]),
            ("Cdim7", [0, 3, 6, 9], ["C", "Eb", "Gb", "Bbb"]),
            ("Dbdim7", [1, 4, 7, 10], ["Db", "Fb", "Abb", "Cbb"]),
            ("Dbaug", [1, 5, 9], ["Db", "F", "A"]),
            ("Eaug", [4, 8, 12], ["E", "G#", "B#"]),
            ("CM9/D", [-10, 0, 4, 7, 11], ["D", "C", "E", "G", "B"]),
            ("Fsus4", [5, 10, 12], ["F", "Bb", "C"]),
            ("G7", [7, 11, 14, 17], ["G", "B", "D", "F"]),
            ("G7b9", [7, 11, 14, 17, 20], ["G", "B", "D", "F", "Ab"]),
            ("G7#11", [7, 11, 14, 17, 25], ["G", "B", "D", "F", "C#"]),
            ("Gm7", [7, 10, 14, 17], ["G", "Bb", "D", "F"]),
            ("C6", [0, 4, 7, 9], ["C", "E", "G", "A"]),
            ("C#m7b9b5", [1, 4, 7, 11, 14], ["C#", "E", "G", "B", "D"]),
            ("Db5", [1, 8], ["Db", "Ab"]),
            ("D(b5)", [2, 6, 8], ["D", "F#", "Ab"]),
            ("Cno5", [0, 4], ["C", "E"]),
            ("Cadd4", [0, 4, 5, 7], ["C", "E", "F", "G"]),
            ("CMadd4", [0, 4, 5, 7], ["C", "E", "F", "G"]),
            ("Cmadd4", [0, 3, 5, 7], ["C", "Eb", "F", "G"]),
            ("Csus4add9", [0, 5, 7, 14], ["C", "F", "G", "D"]),
            ("Cm7add11", [0, 3, 7, 10, 17], ["C", "Eb", "G", "Bb", "F"]),
            ("CM7add11", [0, 4, 7, 11, 17], ["C", "E", "G", "B", "F"]),
            ("Dm7b5", [2, 5, 8, 12], ["D", "F", "Ab", "C"]),
            ("Bm7-5", [11, 14, 17, 21], ["B", "D", "F", "A"]),
            ("Ebm7b5", [3, 6, 9, 13], ["Eb", "Gb", "Bbb", "Db"]),
            ("CmM7add11", [0, 3, 7, 11, 17], ["C", "Eb", "G", "B", "F"]),
            ("CM7add13", [0, 4, 7, 11, 21], ["C", "E", "G", "B", "A"]),
            ("C11", [0, 4, 7, 10, 14, 17], ["C", "E", "G", "Bb", "D", "F"]),
            ("C13", [0, 4, 7, 10, 14, 17, 21], ["C", "E", "G", "Bb", "D", "F", "A"]),
        ]:
            with self.subTest(chord=chord):
                c = Chord(chord)
                self.assertEqual(c.components(visible=False), expected_qualities)
                self.assertEqual(c.components(visible=True), expected_notes)

    def test_major_add9(self):
        # major add 9 is a major chord with a Major ninth
        base = Chord("C")
        base0 = list(base.components(visible=False))
        base1 = list(base.components(visible=True))
        c = Chord("CMadd9")
        com0 = c.components(visible=False)
        self.assertEqual(com0, base0 + [14])
        com1 = c.components(visible=True)
        self.assertEqual(com1, base1 + ["D"])

    def test_too_many_accidentals(self):
        for chord in [
            "A##",
            "B##",
            "D##",
            "E##",
            "Fbb",
            "G##",
        ]:
            with self.subTest(chord=chord):
                c = Chord(chord)
                with self.assertRaises(ValueError) as cm:
                    c.components()
                self.assertEqual(
                    str(cm.exception),
                    f"{chord}maj scale requires too many accidentals",
                )


class TestChordComponentWithPitch(unittest.TestCase):
    def test_basic_chords_with_pitch(self):
        """
        Validates if a chord with pitch is correctly calculated.
        """
        for chord, root_pitch, expected in [
            ("C", 1, ["C1", "E1", "G1"]),
            ("Am", 2, ["A2", "C3", "E3"]),
            ("Dm7/G", 3, ["G3", "D4", "F4", "A4", "C5"]),
            ("Eadd9", 5, ["E5", "G#5", "B5", "F#6"]),
        ]:
            with self.subTest(chord=chord, root_pitch=root_pitch):
                c = Chord(chord)
                self.assertEqual(
                    c.components_with_pitch(root_pitch=root_pitch), expected
                )

    def test_first_order_inversion(self):
        c = Chord("G/1")
        com = c.components_with_pitch(root_pitch=4)
        self.assertEqual(com, ["B4", "D5", "G5"])
        c2 = Chord("G13b9/1")
        com2 = c2.components_with_pitch(root_pitch=4)
        self.assertEqual(com2, ["B4", "D5", "F5", "Ab5", "C6", "E6", "G6"])

    def test_second_order_inversion(self):
        c = Chord("G/2")
        com = c.components_with_pitch(root_pitch=4)
        self.assertEqual(com, ["D5", "G5", "B5"])
        c2 = Chord("G13b9/2")
        com2 = c2.components_with_pitch(root_pitch=4)
        self.assertEqual(com2, ["D5", "F5", "Ab5", "C6", "E6", "G6", "B6"])

    def test_third_order_inversion(self):
        c = Chord("Cm7/3")
        com = c.components_with_pitch(root_pitch=4)
        self.assertEqual(com, ["Bb4", "C5", "Eb5", "G5"])
        c2 = Chord("F#7/3")
        com2 = c2.components_with_pitch(root_pitch=4)
        self.assertEqual(com2, ["E5", "F#5", "A#5", "C#6"])
        c3 = Chord("G13b9/3")
        com3 = c3.components_with_pitch(root_pitch=4)
        self.assertEqual(com3, ["F5", "Ab5", "C6", "E6", "G6", "B6", "D7"])

    def test_fourth_order_inversion(self):
        c = Chord("F7b9")
        com = c.components_with_pitch(root_pitch=4)
        self.assertEqual(com, ["F4", "A4", "C5", "Eb5", "Gb5"])
        c2 = Chord("G13b9/4")
        com2 = c2.components_with_pitch(root_pitch=4)
        self.assertEqual(com2, ["Ab5", "C6", "E6", "G6", "B6", "D7", "F7"])

    def test_fifth_order_inversion(self):
        c = Chord("G13b9/5")
        com = c.components_with_pitch(root_pitch=4)
        self.assertEqual(com, ["C6", "E6", "G6", "B6", "D7", "F7", "Ab7"])


================================================
FILE: test/test_progression.py
================================================
import unittest

from pychord import Chord, ChordProgression


class TestChordProgressionCreations(unittest.TestCase):
    def test_none(self):
        cp = ChordProgression()
        self.assertEqual(cp.chords, [])
        self.assertEqual(str(cp), "")
        self.assertEqual(repr(cp), "<ChordProgression: >")

    def test_one_chord(self):
        c = Chord("C")
        cp = ChordProgression(c)
        self.assertEqual(cp.chords, [c])
        self.assertEqual(str(cp), "C")
        self.assertEqual(repr(cp), "<ChordProgression: C>")

    def test_one_chord_str(self):
        c = "C"
        cp = ChordProgression(c)
        self.assertEqual(cp.chords, [Chord(c)])
        self.assertEqual(str(cp), "C")
        self.assertEqual(repr(cp), "<ChordProgression: C>")

    def test_one_chord_invalid_type(self):
        with self.assertRaises(TypeError):
            ChordProgression(1)

    def test_one_chord_list(self):
        c = Chord("C")
        cp = ChordProgression([c])
        self.assertEqual(cp.chords, [c])

    def test_one_chord_list_str(self):
        c = "C"
        cp = ChordProgression([c])
        self.assertEqual(cp.chords, [Chord(c)])
        self.assertEqual(str(cp), "C")
        self.assertEqual(repr(cp), "<ChordProgression: C>")

    def test_one_chord_list_invalid_type(self):
        with self.assertRaises(TypeError):
            ChordProgression([1])

    def test_multiple_chords(self):
        c1 = Chord("C")
        c2 = Chord("D")
        cp = ChordProgression([c1, c2])
        self.assertEqual(cp.chords, [c1, c2])
        self.assertEqual(str(cp), "C | D")
        self.assertEqual(repr(cp), "<ChordProgression: C | D>")

    def test_multiple_chords_str(self):
        c1 = "C"
        c2 = "D"
        cp = ChordProgression([c1, c2])
        self.assertEqual(cp.chords, [Chord(c1), Chord(c2)])
        self.assertEqual(str(cp), "C | D")
        self.assertEqual(repr(cp), "<ChordProgression: C | D>")


class TestChordProgressionFunctions(unittest.TestCase):
    def test_append(self):
        cp = ChordProgression(["C", "D", "E"])
        cp.append("F")
        self.assertEqual(len(cp), 4)
        self.assertEqual(cp.chords[-1], Chord("F"))

    def test_insert(self):
        cp = ChordProgression(["C", "D", "E"])
        cp.insert(0, "F")
        self.assertEqual(len(cp), 4)
        self.assertEqual(cp.chords[0], Chord("F"))

    def test_pop(self):
        cp = ChordProgression(["C", "D", "E"])
        c = cp.pop()
        self.assertEqual(len(cp), 2)
        self.assertEqual(c, Chord("E"))

    def test_transpose(self):
        cp = ChordProgression(["C", "F", "G"])
        cp.transpose(3)
        self.assertEqual(cp.chords, [Chord("Eb"), Chord("Ab"), Chord("Bb")])

    def test_add(self):
        cp1 = ChordProgression(["C", "F", "G"])
        cp2 = ChordProgression(["Am", "Em"])
        cp = cp1 + cp2
        self.assertEqual(len(cp), 5)
        self.assertEqual(
            cp.chords, [Chord("C"), Chord("F"), Chord("G"), Chord("Am"), Chord("Em")]
        )

        # Check the original progressions have not changed.
        self.assertEqual(len(cp1), 3)
        self.assertEqual(len(cp2), 2)

    def test_self_add(self):
        cp1 = ChordProgression(["C", "F", "G"])
        cp2 = ChordProgression(["Am", "Em"])
        cp1 += cp2
        self.assertEqual(len(cp1), 5)
        self.assertEqual(
            cp1.chords, [Chord("C"), Chord("F"), Chord("G"), Chord("Am"), Chord("Em")]
        )

    def test_get_item(self):
        cp = ChordProgression(["C", "F", "G"])
        self.assertEqual(cp[0], Chord("C"))
        self.assertEqual(cp[1], Chord("F"))
        self.assertEqual(cp[-1], Chord("G"))

    def test_set_item(self):
        cp = ChordProgression(["C", "F", "G"])
        cp[1] = Chord("E")
        self.assertEqual(cp[0], Chord("C"))
        self.assertEqual(cp[1], Chord("E"))
        self.assertEqual(cp[2], Chord("G"))
        self.assertEqual(len(cp), 3)

    def test_slice(self):
        cp = ChordProgression(["C", "F", "G"])
        self.assertEqual(cp[0:1], [Chord("C")])
        self.assertEqual(cp[1:], [Chord("F"), Chord("G")])
        self.assertEqual(cp[0::2], [Chord("C"), Chord("G")])

    def test_eq(self):
        cp1 = ChordProgression(["C", "F", "G"])
        cp2 = ChordProgression(["C", "F", "G"])
        self.assertEqual(cp1, cp2)
        self.assertIsNot(cp1, cp2)

    def test_invalid_eq(self):
        cp = ChordProgression(["C", "F", "G"])
        with self.assertRaises(TypeError):
            print(cp == 0)


================================================
FILE: test/test_quality.py
================================================
# -*- coding: utf-8 -*-

import unittest

from pychord import QualityManager, Chord, find_chords_from_notes


class TestQuality(unittest.TestCase):
    def setUp(self):
        self.quality_manager = QualityManager()

    def test_eq(self):
        q1 = self.quality_manager.get_quality("m7-5")
        q2 = self.quality_manager.get_quality("m7-5")
        self.assertEqual(q1, q2)

    def test_eq_alias_maj9(self):
        q1 = self.quality_manager.get_quality("M9")
        q2 = self.quality_manager.get_quality("maj9")
        self.assertEqual(q1, q2)

    def test_eq_alias_m7b5(self):
        q1 = self.quality_manager.get_quality("m7-5")
        q2 = self.quality_manager.get_quality("m7b5")
        self.assertEqual(q1, q2)

    def test_eq_alias_min(self):
        q1 = self.quality_manager.get_quality("m")
        q2 = self.quality_manager.get_quality("min")
        q3 = self.quality_manager.get_quality("-")
        self.assertEqual(q1, q2)
        self.assertEqual(q1, q3)

    def test_invalid_eq(self):
        q = self.quality_manager.get_quality("m7")
        with self.assertRaises(TypeError):
            print(q == 0)

    def subtest_quality_synonym(self, a, b):
        with self.subTest(msg=f"{a}_has_{b}_synonym"):
            a_quality = self.quality_manager.get_quality(a)
            b_quality = self.quality_manager.get_quality(b)
            self.assertEqual(a_quality, b_quality)

    def test_maj_synonyms(self):
        for q in self.quality_manager.get_qualities():
            if q in ["M", "maj"]:
                continue
            if "maj" in q:
                self.subtest_quality_synonym(q, q.replace("maj", "M"))
            elif "M" in q:
                self.subtest_quality_synonym(q, q.replace("M", "maj"))

    def test_properties(self):
        q = self.quality_manager.get_quality("m")
        self.assertEqual(str(q), "m")
        self.assertEqual(q.intervals, ["1", "b3", "5"])
        self.assertEqual(q.quality, "m")


class TestQualityManager(unittest.TestCase):
    def test_singleton(self):
        quality_manager = QualityManager()
        quality_manager2 = QualityManager()
        self.assertIs(quality_manager, quality_manager2)


class TestOverwriteQuality(unittest.TestCase):
    def setUp(self):
        self.quality_manager = QualityManager()

    def tearDown(self):
        self.quality_manager.load_default_qualities()

    def test_overwrite(self):
        # Remove the 9th from the "11" quality before building a chord.
        self.quality_manager.set_quality("11", ("1", "3", "5", "b7", "11"))
        chord = Chord("C11")
        self.assertEqual(chord.components(), ["C", "E", "G", "Bb", "F"])

    def test_find_from_components(self):
        # Remove the 9th from the "11" quality then lookup a chord.
        self.quality_manager.set_quality("11", ("1", "3", "5", "b7", "11"))
        chords = find_chords_from_notes(["C", "E", "G", "Bb", "F"])
        self.assertEqual(chords, [Chord("C11")])

    def test_keep_existing_chord(self):
        # Remove the 9th from the "11" quality after building a chord.
        chord = Chord("C11")
        self.quality_manager.set_quality("11", ("1", "3", "5", "b7", "11"))
        self.assertEqual(chord.components(), ["C", "E", "G", "Bb", "D", "F"])


class TestIterateQualities(unittest.TestCase):
    def setUp(self):
        self.quality_manager = QualityManager()

    def tearDown(self):
        self.quality_manager.load_default_qualities()

    def test_iterate_qualities(self):
        assert "m" in self.quality_manager.get_qualities()

    def test_immutable_qualities(self):
        qualities = self.quality_manager.get_qualities()
        assert "testquality" not in qualities
        qualities["testquality"] = qualities["m"]
        qualities = self.quality_manager.get_qualities()
        assert "testquality" not in qualities

    def test_iterate_added_qualities(self):
        self.quality_manager.set_quality("testquality", ("1",))
        qualities = self.quality_manager.get_qualities()
        assert "testquality" in qualities


================================================
FILE: test/test_transpose.py
================================================
import unittest

from pychord import Chord


class TestChordTranspose(unittest.TestCase):
    def test_transpose_zero(self):
        c = Chord("Am")
        c.transpose(0)
        self.assertEqual(c.root, "A")
        self.assertEqual(c.quality.quality, "m")
        self.assertEqual(c, Chord("Am"))

    def test_transpose_positive(self):
        c = Chord("Am")
        c.transpose(3)
        self.assertEqual(c.root, "C")
        self.assertEqual(c.quality.quality, "m")
        self.assertEqual(c, Chord("Cm"))

    def test_transpose_negative(self):
        c = Chord("Am")
        c.transpose(-4)
        self.assertEqual(c.root, "F")
        self.assertEqual(c.quality.quality, "m")
        self.assertEqual(c, Chord("Fm"))

    def test_transpose_slash(self):
        c = Chord("Am7/G")
        c.transpose(3)
        self.assertEqual(c.root, "C")
        self.assertEqual(c.quality.quality, "m7")
        self.assertEqual(c.on, "Bb")
        self.assertEqual(c._chord, Chord("Cm7/Bb")._chord)
        self.assertEqual(c.quality.components, Chord("Cm7/Bb").quality.components)
        self.assertEqual(c, Chord("Cm7/Bb"))

    def test_transpose_inversion(self):
        c = Chord("Am7/3")
        c.transpose(3)
        self.assertEqual(c.root, "C")
        self.assertEqual(c.quality.quality, "m7")

    def test_invalid_transpose_type(self):
        c = Chord("Am")
        self.assertRaises(TypeError, c.transpose, "A")

    def test_transpose_eq1(self):
        c = Chord("C")
        c.transpose(1)
        self.assertEqual(c, Chord("C#"))
        self.assertEqual(c, Chord("Db"))

    def test_transpose_eq2(self):
        c = Chord("C")
        c.transpose(2)
        self.assertEqual(c, Chord("D"))


================================================
FILE: test/test_utils.py
================================================
import unittest

from pychord.utils import augment, diminish, note_to_val


class TestUtils(unittest.TestCase):
    def test_augment(self):
        self.assertEqual(augment("Cb"), "C")
        self.assertEqual(augment("C"), "C#")
        self.assertEqual(augment("C#"), "C##")

    def test_diminish(self):
        self.assertEqual(diminish("Cb"), "Cbb")
        self.assertEqual(diminish("C"), "Cb")
        self.assertEqual(diminish("C#"), "C")

    def test_note_to_val(self):
        self.assertEqual(note_to_val("C"), 0)

    def test_note_to_val_invalid(self):
        with self.assertRaises(ValueError):
            note_to_val("X")
Download .txt
gitextract_ch1k6q74/

├── .github/
│   └── workflows/
│       ├── build.yml
│       ├── codeql-analysis.yml
│       └── deploy.yml
├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── README.md
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   ├── pychord.rst
│   └── requirements.txt
├── examples/
│   └── pychord-midi.py
├── pychord/
│   ├── __init__.py
│   ├── analyzer.py
│   ├── chord.py
│   ├── constants/
│   │   ├── __init__.py
│   │   ├── qualities.py
│   │   └── scales.py
│   ├── parser.py
│   ├── progression.py
│   ├── py.typed
│   ├── quality.py
│   └── utils.py
├── pyproject.toml
├── setup.cfg
├── setup.py
└── test/
    ├── __init__.py
    ├── test_analyzer.py
    ├── test_chord.py
    ├── test_component.py
    ├── test_progression.py
    ├── test_quality.py
    ├── test_transpose.py
    └── test_utils.py
Download .txt
SYMBOL INDEX (168 symbols across 14 files)

FILE: examples/pychord-midi.py
  function create_midi (line 11) | def create_midi(chords):
  function main (line 25) | def main():

FILE: pychord/analyzer.py
  function find_chords_from_notes (line 6) | def find_chords_from_notes(notes: list[str]) -> list[Chord]:
  function notes_to_positions (line 34) | def notes_to_positions(notes: list[str], root: str) -> list[int]:
  function get_all_rotated_notes (line 56) | def get_all_rotated_notes(notes: list[str]) -> list[list[str]]:

FILE: pychord/chord.py
  class Chord (line 9) | class Chord:
    method __init__ (line 16) | def __init__(self, chord: str) -> None:
    method __str__ (line 23) | def __str__(self) -> str:
    method __repr__ (line 26) | def __repr__(self) -> str:
    method __eq__ (line 29) | def __eq__(self, other: Any) -> bool:
    method from_note_index (line 52) | def from_note_index(
    method chord (line 129) | def chord(self) -> str:
    method root (line 136) | def root(self) -> str:
    method quality (line 143) | def quality(self) -> Quality:
    method on (line 150) | def on(self) -> str:
    method info (line 156) | def info(self) -> str:
    method transpose (line 165) | def transpose(self, trans: int, scale: str = "C") -> None:
    method components (line 180) | def components(self, visible: Literal[True]) -> list[str]: ...
    method components (line 183) | def components(self, visible: Literal[False]) -> list[int]: ...
    method components (line 185) | def components(self, visible: bool = True) -> list[str] | list[int]:
    method components_with_pitch (line 207) | def components_with_pitch(self, root_pitch: int) -> list[str]:
    method _reconfigure_chord (line 219) | def _reconfigure_chord(self) -> None:

FILE: pychord/parser.py
  function _check_mode (line 12) | def _check_mode(mode: str) -> None:
  function _check_note (line 18) | def _check_note(note: str) -> None:
  function parse (line 24) | def parse(chord: str) -> tuple[str, Quality, str]:
  function parse_scale (line 61) | def parse_scale(scale: str) -> tuple[str, str]:

FILE: pychord/progression.py
  class ChordProgression (line 6) | class ChordProgression:
    method __init__ (line 13) | def __init__(
    method __str__ (line 28) | def __str__(self) -> str:
    method __repr__ (line 31) | def __repr__(self) -> str:
    method __add__ (line 34) | def __add__(self, other: "ChordProgression") -> "ChordProgression":
    method __len__ (line 37) | def __len__(self) -> int:
    method __getitem__ (line 40) | def __getitem__(self, key: int) -> Chord:
    method __setitem__ (line 43) | def __setitem__(self, key: int, value: Chord) -> None:
    method __eq__ (line 46) | def __eq__(self, other: Any) -> bool:
    method chords (line 54) | def chords(self) -> list[Chord]:
    method append (line 60) | def append(self, chord: str | Chord) -> None:
    method insert (line 68) | def insert(self, index: int, chord: str | Chord) -> None:
    method pop (line 77) | def pop(self, index: int = -1) -> Chord:
    method transpose (line 85) | def transpose(self, trans: int) -> None:
    method _as_chord (line 95) | def _as_chord(chord: str | Chord) -> Chord:

FILE: pychord/quality.py
  class Quality (line 11) | class Quality:
    method __init__ (line 24) | def __init__(self, name: str, intervals: tuple[str, ...]) -> None:
    method __str__ (line 28) | def __str__(self) -> str:
    method __eq__ (line 31) | def __eq__(self, other: Any) -> bool:
    method components (line 37) | def components(self) -> tuple[int, ...]:
    method intervals (line 41) | def intervals(self) -> list[str]:
    method quality (line 48) | def quality(self) -> str:
    method get_components (line 55) | def get_components(self, root: str, visible: Literal[True]) -> list[st...
    method get_components (line 58) | def get_components(self, root: str, visible: Literal[False]) -> list[i...
    method get_components (line 61) | def get_components(self, root: str, visible: bool) -> list[str] | list...
    method get_components (line 63) | def get_components(
  class QualityManager (line 80) | class QualityManager:
    method __new__ (line 85) | def __new__(cls) -> "QualityManager":
    method load_default_qualities (line 91) | def load_default_qualities(self) -> None:
    method get_quality (line 94) | def get_quality(self, name: str, inversion: int = 0) -> Quality:
    method get_qualities (line 108) | def get_qualities(self) -> dict[str, Quality]:
    method set_quality (line 111) | def set_quality(self, name: str, intervals: tuple[str, ...]) -> None:
    method find_quality_from_components (line 122) | def find_quality_from_components(self, components: list[int]) -> Quali...
  function _apply_interval_to_note (line 134) | def _apply_interval_to_note(root: str, interval: str) -> str:
  function _get_interval_pitch (line 148) | def _get_interval_pitch(interval: str) -> int:
  function _parse_interval (line 160) | def _parse_interval(interval: str) -> tuple[str, int]:
  function scale_notes (line 169) | def scale_notes(root: str, mode: str) -> list[str]:

FILE: pychord/utils.py
  function augment (line 4) | def augment(note: str) -> str:
  function diminish (line 14) | def diminish(note: str) -> str:
  function note_to_val (line 24) | def note_to_val(note: str) -> int:
  function transpose_note (line 44) | def transpose_note(note: str, transpose: int, scale: str = "C") -> str:

FILE: test/test_analyzer.py
  class TestNotesToPositions (line 11) | class TestNotesToPositions(unittest.TestCase):
    method test_notes_to_positions (line 12) | def test_notes_to_positions(self):
  class TestGetAllRotatedNotes (line 27) | class TestGetAllRotatedNotes(unittest.TestCase):
    method test_get_all_rotated_notes (line 28) | def test_get_all_rotated_notes(self):
  class TestFindChordsFromNotes (line 37) | class TestFindChordsFromNotes(unittest.TestCase):
    method test_empty (line 38) | def test_empty(self):
    method test_find_chords_from_notes (line 42) | def test_find_chords_from_notes(self):
    method test_idempotence (line 67) | def test_idempotence(self):

FILE: test/test_chord.py
  class TestChordCreations (line 6) | class TestChordCreations(unittest.TestCase):
    method test_chord_creation (line 7) | def test_chord_creation(self):
    method test_invalid_chord (line 21) | def test_invalid_chord(self):
    method test_slash_chord (line 35) | def test_slash_chord(self):
    method test_invalid_slash_chord (line 46) | def test_invalid_slash_chord(self):
    method test_inversion (line 49) | def test_inversion(self):
    method test_eq (line 62) | def test_eq(self):
    method test_eq_quality_alias (line 66) | def test_eq_quality_alias(self):
    method test_eq_root_alias (line 69) | def test_eq_root_alias(self):
    method test_eq_invalid (line 72) | def test_eq_invalid(self):
    method test_eq_different_root (line 76) | def test_eq_different_root(self):
    method test_eq_different_quality (line 79) | def test_eq_different_quality(self):
    method test_eq_different_on (line 82) | def test_eq_different_on(self):
    method test_components (line 87) | def test_components(self):
    method test_info (line 93) | def test_info(self):
  class TestChordFromNoteIndex (line 115) | class TestChordFromNoteIndex(unittest.TestCase):
    method test_from_note_index (line 116) | def test_from_note_index(self):
    method test_from_note_index_with_chromatic (line 127) | def test_from_note_index_with_chromatic(self):
    method test_invalid_note_index (line 140) | def test_invalid_note_index(self):
    method test_invalid_scale (line 150) | def test_invalid_scale(self):
    method test_diatonic_from_note_index (line 160) | def test_diatonic_from_note_index(self):
    method test_diatonic_note_non_generic (line 173) | def test_diatonic_note_non_generic(self):

FILE: test/test_component.py
  class TestChordComponent (line 6) | class TestChordComponent(unittest.TestCase):
    method test_chord_components (line 7) | def test_chord_components(self):
    method test_major_add9 (line 86) | def test_major_add9(self):
    method test_too_many_accidentals (line 97) | def test_too_many_accidentals(self):
  class TestChordComponentWithPitch (line 116) | class TestChordComponentWithPitch(unittest.TestCase):
    method test_basic_chords_with_pitch (line 117) | def test_basic_chords_with_pitch(self):
    method test_first_order_inversion (line 133) | def test_first_order_inversion(self):
    method test_second_order_inversion (line 141) | def test_second_order_inversion(self):
    method test_third_order_inversion (line 149) | def test_third_order_inversion(self):
    method test_fourth_order_inversion (line 160) | def test_fourth_order_inversion(self):
    method test_fifth_order_inversion (line 168) | def test_fifth_order_inversion(self):

FILE: test/test_progression.py
  class TestChordProgressionCreations (line 6) | class TestChordProgressionCreations(unittest.TestCase):
    method test_none (line 7) | def test_none(self):
    method test_one_chord (line 13) | def test_one_chord(self):
    method test_one_chord_str (line 20) | def test_one_chord_str(self):
    method test_one_chord_invalid_type (line 27) | def test_one_chord_invalid_type(self):
    method test_one_chord_list (line 31) | def test_one_chord_list(self):
    method test_one_chord_list_str (line 36) | def test_one_chord_list_str(self):
    method test_one_chord_list_invalid_type (line 43) | def test_one_chord_list_invalid_type(self):
    method test_multiple_chords (line 47) | def test_multiple_chords(self):
    method test_multiple_chords_str (line 55) | def test_multiple_chords_str(self):
  class TestChordProgressionFunctions (line 64) | class TestChordProgressionFunctions(unittest.TestCase):
    method test_append (line 65) | def test_append(self):
    method test_insert (line 71) | def test_insert(self):
    method test_pop (line 77) | def test_pop(self):
    method test_transpose (line 83) | def test_transpose(self):
    method test_add (line 88) | def test_add(self):
    method test_self_add (line 101) | def test_self_add(self):
    method test_get_item (line 110) | def test_get_item(self):
    method test_set_item (line 116) | def test_set_item(self):
    method test_slice (line 124) | def test_slice(self):
    method test_eq (line 130) | def test_eq(self):
    method test_invalid_eq (line 136) | def test_invalid_eq(self):

FILE: test/test_quality.py
  class TestQuality (line 8) | class TestQuality(unittest.TestCase):
    method setUp (line 9) | def setUp(self):
    method test_eq (line 12) | def test_eq(self):
    method test_eq_alias_maj9 (line 17) | def test_eq_alias_maj9(self):
    method test_eq_alias_m7b5 (line 22) | def test_eq_alias_m7b5(self):
    method test_eq_alias_min (line 27) | def test_eq_alias_min(self):
    method test_invalid_eq (line 34) | def test_invalid_eq(self):
    method subtest_quality_synonym (line 39) | def subtest_quality_synonym(self, a, b):
    method test_maj_synonyms (line 45) | def test_maj_synonyms(self):
    method test_properties (line 54) | def test_properties(self):
  class TestQualityManager (line 61) | class TestQualityManager(unittest.TestCase):
    method test_singleton (line 62) | def test_singleton(self):
  class TestOverwriteQuality (line 68) | class TestOverwriteQuality(unittest.TestCase):
    method setUp (line 69) | def setUp(self):
    method tearDown (line 72) | def tearDown(self):
    method test_overwrite (line 75) | def test_overwrite(self):
    method test_find_from_components (line 81) | def test_find_from_components(self):
    method test_keep_existing_chord (line 87) | def test_keep_existing_chord(self):
  class TestIterateQualities (line 94) | class TestIterateQualities(unittest.TestCase):
    method setUp (line 95) | def setUp(self):
    method tearDown (line 98) | def tearDown(self):
    method test_iterate_qualities (line 101) | def test_iterate_qualities(self):
    method test_immutable_qualities (line 104) | def test_immutable_qualities(self):
    method test_iterate_added_qualities (line 111) | def test_iterate_added_qualities(self):

FILE: test/test_transpose.py
  class TestChordTranspose (line 6) | class TestChordTranspose(unittest.TestCase):
    method test_transpose_zero (line 7) | def test_transpose_zero(self):
    method test_transpose_positive (line 14) | def test_transpose_positive(self):
    method test_transpose_negative (line 21) | def test_transpose_negative(self):
    method test_transpose_slash (line 28) | def test_transpose_slash(self):
    method test_transpose_inversion (line 38) | def test_transpose_inversion(self):
    method test_invalid_transpose_type (line 44) | def test_invalid_transpose_type(self):
    method test_transpose_eq1 (line 48) | def test_transpose_eq1(self):
    method test_transpose_eq2 (line 54) | def test_transpose_eq2(self):

FILE: test/test_utils.py
  class TestUtils (line 6) | class TestUtils(unittest.TestCase):
    method test_augment (line 7) | def test_augment(self):
    method test_diminish (line 12) | def test_diminish(self):
    method test_note_to_val (line 17) | def test_note_to_val(self):
    method test_note_to_val_invalid (line 20) | def test_note_to_val_invalid(self):
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (89K chars).
[
  {
    "path": ".github/workflows/build.yml",
    "chars": 909,
    "preview": "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 "
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 663,
    "preview": "name: CodeQL\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  schedule:\n    - cron: '0 2 * * 1'\n\njobs:\n  analyz"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 601,
    "preview": "name: Publish Python package\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n"
  },
  {
    "path": ".gitignore",
    "chars": 797,
    "preview": ".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"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 172,
    "preview": "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 "
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 3311,
    "preview": "![PyChord](https://github.com/yuma-m/pychord/raw/main/pychord.png)\n\n# PyChord ![Build Status](https://github.com/yuma-m/"
  },
  {
    "path": "docs/Makefile",
    "chars": 8327,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/conf.py",
    "chars": 5290,
    "preview": "# -*- coding: utf-8 -*-\n#\n# pychord documentation build configuration file, created by\n# sphinx-quickstart on Sat Dec 31"
  },
  {
    "path": "docs/index.rst",
    "chars": 448,
    "preview": ".. pychord documentation master file, created by\n   sphinx-quickstart on Sat Dec 31 14:51:42 2016.\n   You can adapt this"
  },
  {
    "path": "docs/pychord.rst",
    "chars": 70,
    "preview": "pychord package\n===============\n\n.. automodule:: pychord\n   :members:\n"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 38,
    "preview": "sphinx==9.1.0\nsphinx_rtd_theme==3.1.0\n"
  },
  {
    "path": "examples/pychord-midi.py",
    "chars": 985,
    "preview": "# An example to create MIDI file with PyChord and pretty_midi\n# Prerequisite: pip install pretty_midi\n# pretty_midi: htt"
  },
  {
    "path": "pychord/__init__.py",
    "chars": 276,
    "preview": "from .analyzer import find_chords_from_notes\nfrom .chord import Chord\nfrom .progression import ChordProgression\nfrom .qu"
  },
  {
    "path": "pychord/analyzer.py",
    "chars": 1962,
    "preview": "from .chord import Chord\nfrom .quality import QualityManager\nfrom .utils import note_to_val\n\n\ndef find_chords_from_notes"
  },
  {
    "path": "pychord/chord.py",
    "chars": 8118,
    "preview": "from typing import Any, Literal, overload\n\nfrom .constants.scales import RELATIVE_KEY_DICT\nfrom .parser import parse, pa"
  },
  {
    "path": "pychord/constants/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pychord/constants/qualities.py",
    "chars": 4633,
    "preview": "# Do not import DEFAULT_QUALITIES directly\n# Use QualityManager instead\nDEFAULT_QUALITIES = [\n    # chords consist of 2 "
  },
  {
    "path": "pychord/constants/scales.py",
    "chars": 1300,
    "preview": "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 = "
  },
  {
    "path": "pychord/parser.py",
    "chars": 1743,
    "preview": "import re\n\nfrom .constants.scales import RELATIVE_KEY_DICT\nfrom .quality import QualityManager, Quality\n\n# We accept not"
  },
  {
    "path": "pychord/progression.py",
    "chars": 3209,
    "preview": "from typing import Any\n\nfrom .chord import Chord\n\n\nclass ChordProgression:\n    \"\"\"\n    A chord progression, which is a s"
  },
  {
    "path": "pychord/py.typed",
    "chars": 7,
    "preview": "Marker\n"
  },
  {
    "path": "pychord/quality.py",
    "chars": 6395,
    "preview": "import copy\nimport functools\nimport re\nfrom typing import Any, Literal, overload\n\nfrom .constants.qualities import DEFAU"
  },
  {
    "path": "pychord/utils.py",
    "chars": 1076,
    "preview": "from .constants.scales import NOTE_VALUES, SCALE_VAL_DICT\n\n\ndef augment(note: str) -> str:\n    \"\"\"\n    Augment the given"
  },
  {
    "path": "pyproject.toml",
    "chars": 1136,
    "preview": "[build-system]\nrequires = [\"setuptools>=45\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"pychord"
  },
  {
    "path": "setup.cfg",
    "chars": 55,
    "preview": "[metadata]\nname = pychord\n\n[bdist_wheel]\nuniversal = 0\n"
  },
  {
    "path": "setup.py",
    "chars": 69,
    "preview": "from setuptools import setup\n\nif __name__ == \"__main__\":\n    setup()\n"
  },
  {
    "path": "test/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/test_analyzer.py",
    "chars": 3190,
    "preview": "import unittest\n\nfrom pychord import Chord\nfrom pychord.analyzer import (\n    get_all_rotated_notes,\n    find_chords_fro"
  },
  {
    "path": "test/test_chord.py",
    "chars": 6396,
    "preview": "import unittest\n\nfrom pychord import Chord\n\n\nclass TestChordCreations(unittest.TestCase):\n    def test_chord_creation(se"
  },
  {
    "path": "test/test_component.py",
    "chars": 7704,
    "preview": "import unittest\n\nfrom pychord import Chord\n\n\nclass TestChordComponent(unittest.TestCase):\n    def test_chord_components("
  },
  {
    "path": "test/test_progression.py",
    "chars": 4535,
    "preview": "import unittest\n\nfrom pychord import Chord, ChordProgression\n\n\nclass TestChordProgressionCreations(unittest.TestCase):\n "
  },
  {
    "path": "test/test_quality.py",
    "chars": 4069,
    "preview": "# -*- coding: utf-8 -*-\n\nimport unittest\n\nfrom pychord import QualityManager, Chord, find_chords_from_notes\n\n\nclass Test"
  },
  {
    "path": "test/test_transpose.py",
    "chars": 1716,
    "preview": "import unittest\n\nfrom pychord import Chord\n\n\nclass TestChordTranspose(unittest.TestCase):\n    def test_transpose_zero(se"
  },
  {
    "path": "test/test_utils.py",
    "chars": 640,
    "preview": "import unittest\n\nfrom pychord.utils import augment, diminish, note_to_val\n\n\nclass TestUtils(unittest.TestCase):\n    def "
  }
]

About this extraction

This page contains the full source code of the yuma-m/pychord GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (79.0 KB), approximately 25.0k tokens, and a symbol index with 168 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!