diff --git a/.codeclimate.yml b/.codeclimate.yml
deleted file mode 100644
index 094144a5f5..0000000000
--- a/.codeclimate.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-# Save as .codeclimate.yml (note leading .) in project root directory
-languages:
- Ruby: false
- JavaScript: true
- PHP: false
- Python: true
-exclude_paths:
- - "music21/ext"
\ No newline at end of file
diff --git a/.coveragerc b/.coveragerc
index f39fe0f5cd..a134e125e3 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -7,6 +7,8 @@ omit =
[report]
exclude_lines =
+ if TYPE_CHECKING:
+ if t.TYPE_CHECKING:
if __name__ == .__main__.:
if debug:
if self.debug:
@@ -14,3 +16,6 @@ exclude_lines =
def x_test
def xtest
pragma: no cover
+ class TestExternal(unittest.TestCase):
+ class TestSlow(unittest.TestCase):
+
diff --git a/.flake8 b/.flake8
index a55e56a59c..e24575d3e6 100644
--- a/.flake8
+++ b/.flake8
@@ -1,19 +1,33 @@
[flake8]
ignore=
- E127, # over indented
- E128, # under indented
- E301, # 0 blank lines -- good test but something going wrong in new algorithm
- E302, # blank lines
- E303, # blank lines
- E501, # let pylint check line length
- E722, # let pylint check bare except
- E731, # do not assign a lambda
- F401, # let pylint check for imported but unused (wildcards and exports trigger)
- F405, # __all__ is okay to get from wildcard.
- F821, # let pylint check for undefined (del at end of module makes undefined)
- F841, # let pylint check for unused
- W391, # extra blank lines at end of file
- W503, # line break BEFORE binary operator
+ # over indented
+ E127,
+ # under indented
+ E128,
+ # 0 blank lines -- good test but something going wrong in new algorithm
+ E301,
+ # blank lines
+ E302,
+ # blank lines
+ E303,
+ # let pylint check line length
+ E501,
+ # let pylint check bare except
+ E722,
+ # do not assign a lambda
+ E731,
+ # let pylint check for imported but unused (wildcards and exports trigger)
+ F401,
+ # __all__ is okay to get from wildcard.
+ F405,
+ # let pylint check for undefined (del at end of module makes undefined)
+ F821,
+ # let pylint check for unused
+ F841,
+ # extra blank lines at end of file
+ W391,
+ # line break BEFORE binary operator
+ W503,
exclude=
.git,
@@ -30,3 +44,7 @@ per-file-ignores =
music21/search/__init__.py:F403
max-line-length=100
+
+inline-quotes = single
+multiline-quotes = '''
+docstring-quotes = '''
diff --git a/.github/scripts/install_ubuntu_deps.sh b/.github/scripts/install_ubuntu_deps.sh
index d763ab6330..31a3751940 100755
--- a/.github/scripts/install_ubuntu_deps.sh
+++ b/.github/scripts/install_ubuntu_deps.sh
@@ -9,10 +9,6 @@ sudo apt-get install -y libpng-dev
wget -q https://lilypond.org/download/binaries/linux-64/lilypond-2.22.1-1.linux-64.sh
sh lilypond-2.22.1-1.linux-64.sh --batch
export PATH=/home/runner/bin:$PATH
-pip3 install -r requirements.txt
-pip3 install coveralls
-pip3 install scipy
-pip3 install python-Levenshtein
-pip3 install setuptools
-pip3 install coverage
+pip3 install wheel
+pip3 install -r requirements_dev.txt
python3 -m compileall music21
diff --git a/.github/workflows/maincheck.yml b/.github/workflows/maincheck.yml
index 72f6fca5fc..ab4f0f87c5 100644
--- a/.github/workflows/maincheck.yml
+++ b/.github/workflows/maincheck.yml
@@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.7, 3.8, 3.9, "3.10"]
+ python-version: ["3.10.6", "3.10", "3.11"] # "3.10" must be in quotes to not have it eval to 3.1
steps:
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/checkout@v2
@@ -27,8 +27,69 @@ jobs:
- name: Run Main Test script
run: python -c 'from music21.test.testSingleCoreAll import ciMain as ci; ci()'
- name: Coveralls
- if: ${{ matrix.python-version == '3.7' }}
+ if: ${{ matrix.python-version == '3.10.6' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github
run: coveralls
+
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ cache: 'pip'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install wheel
+ pip install -r requirements.txt
+ pip install -r requirements_dev.txt
+ - name: Install music21 in editable mode
+ run: |
+ python -m pip install -e .
+ - name: Lint with pylint
+ run: |
+ pylint -j0 music21
+ pylint -j0 documentation
+
+ flake:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ cache: 'pip'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install wheel
+ pip install -r requirements.txt
+ pip install -r requirements_dev.txt
+ - name: PEP8 with flake8
+ run: |
+ flake8 music21
+ flake8 documentation
+
+ mypy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ cache: 'pip'
+ - name: Install dependencies
+ run: |
+ pip install wheel
+ python -m pip install -r requirements.txt
+ python -m pip install -r requirements_dev.txt
+ - name: Type-check all modules with mypy
+ run: |
+ mypy music21
diff --git a/.github/workflows/pythonpylint.yml b/.github/workflows/pythonpylint.yml
deleted file mode 100644
index 31c07f4680..0000000000
--- a/.github/workflows/pythonpylint.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-name: PyLint
-
-on:
- push:
- branches:
- - master
- - '*CI*'
- pull_request:
- branches:
- - '*'
-
-jobs:
- lint:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - name: Set up Python
- uses: actions/setup-python@v1
- with:
- python-version: '3.8'
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install IPython
- pip install pylint
- - name: Install music21 in editable mode
- run: |
- python -m pip install -e .
- - name: Lint with pylint
- run: |
- pylint -j0 music21
- pylint -j0 documentation
-
- flake:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - name: Set up Python
- uses: actions/setup-python@v1
- with:
- python-version: '3.8'
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install flake8
- - name: PEP8 with flake8
- run: |
- flake8 music21
- flake8 documentation
-
- mypy:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - name: Set up Python
- uses: actions/setup-python@v1
- with:
- python-version: '3.9'
- - name: Install mypy
- run: |
- python -m pip install mypy
- - name: Type-check certain modules with mypy
- run: |
- mypy --follow-imports=silent music21/capella music21/common music21/corpus music21/features music21/figuredBass music21/humdrum music21/ipython21 music21/languageExcerpts music21/lily music21/mei music21/metadata music21/musedata music21/noteworthy music21/omr music21/romanText music21/test music21/vexflow
- mypy --follow-imports=silent music21/articulations.py music21/bar.py music21/beam.py music21/clef.py music21/configure.py music21/defaults.py music21/derivation.py music21/duration.py music21/dynamics.py music21/editorial.py music21/environment.py music21/exceptions21.py music21/expressions.py music21/freezeThaw.py music21/harmony.py music21/instrument.py music21/interval.py music21/layout.py music21/percussion.py music21/prebase.py music21/repeat.py music21/roman.py music21/serial.py music21/sieve.py music21/sites.py music21/sorting.py music21/spanner.py music21/style.py music21/tablature.py music21/tempo.py music21/text.py music21/tie.py music21/tinyNotation.py music21/variant.py music21/voiceLeading.py music21/volpiano.py music21/volume.py
diff --git a/.gitignore b/.gitignore
index 86eccc1d62..04aa7dd0e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ __pycache__/
# OSX
.DS_Store
+**/.DS_Store
# C extensions
*.so
@@ -40,6 +41,8 @@ develop-eggs
lib
lib64
__pycache__
+**/__pycache__
+venv
# Installer logs
pip-log.txt
@@ -93,3 +96,5 @@ music21/monkeytype.sqlite3
.pytest_cache/v/cache/nodeids
.pytest_cache/v/cache/lastfailed
.pytest_cache/v/cache/nodeids
+.dmypy.json
+*.txt~
diff --git a/.gitignore_global b/.gitignore_global
deleted file mode 100644
index 780245818b..0000000000
--- a/.gitignore_global
+++ /dev/null
@@ -1,3 +0,0 @@
-.DS_Store
-__pycache__
-
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index f94c7d8c1d..5149f08b6d 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -6,6 +6,41 @@
+
+
+
@@ -26,6 +61,7 @@
+
diff --git a/.pylintrc b/.pylintrc
index f3df3c4553..c5b8cb1537 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -7,9 +7,6 @@
# pygtk.require().
# init-hook=
-# Profiled execution.
-profile=no
-
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
@@ -65,22 +62,18 @@ disable=
fixme, # obviously known
superfluous-parens, # nope -- if they make things clearer...
too-many-statements, # someday
- no-member, # important, but too many false positives
too-many-arguments, # definitely! but takes too long to get a fix now...
too-many-public-methods, # maybe...
too-many-branches, # yes, someday
too-many-locals, # no
too-many-lines, # yes, someday
- bad-whitespace, # maybe later, but "bad" isn't something I necessarily agree with
- bad-continuation, # never remove -- this is a good thing many times.
too-many-return-statements, # we'll see
too-many-instance-attributes, # maybe later
+ # no-self-use, # moved to optional extension.
invalid-name, # these are good music21 names; fix the regexp instead...
- no-self-use, # maybe later
too-few-public-methods, # never remove or set to 1
trailing-whitespace, # should ignore blank lines with tabs
missing-docstring, # gets too many well-documented properties
- star-args, # no problem with them...
protected-access, # this is an important one, but see below...
unused-argument,
import-self, # fix is either to get rid of it or move away many tests...
@@ -105,6 +98,8 @@ disable=
not-callable, # false positives, for instance on x.next()
raise-missing-from, # want to do this eventually, but adding 1000 msgs not helpful
consider-using-f-string, # future?
+ unnecessary-lambda-assignment, # opinionated
+ consider-using-generator, # generators are less performant for small container sizes, like most of ours
# 'protected-access', # this is an important one, but for now we do a lot of
# # x = copy.deepcopy(self); x._volume = ... which is not a problem...
@@ -118,11 +113,6 @@ disable=
output-format=text
msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
-files-output=no
-
# Tells whether to display a full report or only the messages
reports=no
@@ -147,9 +137,6 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
# Required attributes for module, separated by a comma
# required-attributes=
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=map,filter,input
-
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
@@ -166,63 +153,33 @@ include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][A-Za-z0-9_]{2,30}$
-# Naming hint for function names
-function-name-hint=[a-z_][A-Za-z0-9_]{2,30}$
-
# Regular expression matching correct variable names
variable-rgx=[a-z_][A-Za-z0-9_]{2,30}$
-# Naming hint for variable names
-variable-name-hint=[a-z_][A-Za-z0-9_]{2,30}$
-
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-# Naming hint for constant names
-const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
# Regular expression matching correct attribute names
attr-rgx=[a-z_][A-Za-z0-9_]{2,30}$
-# Naming hint for attribute names
-attr-name-hint=[a-z_][A-Za-z0-9_]{2,30}$
-
# Regular expression matching correct argument names
argument-rgx=[a-z_][A-Za-z0-9_]{2,30}$
-# Naming hint for argument names
-argument-name-hint=[a-z_][A-Za-z0-9_]{2,30}$
-
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-# Naming hint for class attribute names
-class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-# Naming hint for inline iteration names
-inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
-
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
-# Naming hint for class names
-class-name-hint=[A-Z_][a-zA-Z0-9]+$
-
# Regular expression matching correct module names
module-rgx=(([a-z_][a-zA-Z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-# Naming hint for module names
-module-name-hint=(([a-z_][a-zA-Z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
# Regular expression matching correct method names
method-rgx=[a-z_][a-zA-Z0-9_]{2,30}$
-# Naming hint for method names
-method-name-hint=[a-z_][a-zA-Z0-9_]{2,30}$
-
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__
@@ -244,9 +201,6 @@ ignore-long-lines=^\s*(# )??$|converter\.parse
# else.
single-line-if-stmt=no
-# List of optional constructs for which whitespace checking is disabled
-no-space-check=trailing-comma,dict-separator
-
# Maximum number of lines in a module
max-module-lines=1000
@@ -319,11 +273,7 @@ ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
-ignored-classes=SQLObject
-
-# When zope mode is activated, add a predefined set of Zope acquired attributes
-# to generated-members.
-# zope=no
+ignored-classes=StreamCore
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
@@ -428,4 +378,4 @@ int-import-graph=
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
-overgeneral-exceptions=Exception
+# overgeneral-exceptions=Exception
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000000..3bcf0f99ab
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,27 @@
+# Read the Docs configuration file for music21
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the OS, Python version and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+# Build documentation in the "docs/" directory with Sphinx
+sphinx:
+ configuration: documentation/source/conf.py
+
+# Optionally build your docs in additional formats such as PDF and ePub
+# formats:
+# - pdf
+# - epub
+
+# Optional but recommended, declare the Python requirements required
+# to build your documentation
+# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+# python:
+# install:
+# - requirements: docs/requirements.txt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f57c401c8b..bd4557defd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,7 +1,12 @@
-`music21` welcomes contributions such as bug reports, new features, fixes, and
+`Music21` welcomes contributions such as bug reports, new features, fixes, and
documentation improvements. The
[project repository](http://www.github.com/cuthbertLab/music21) is hosted at GitHub.
+In the first few years of `music21`, major new areas of investigation (medieval
+rhythm encoding, contour analysis, etc.) were routinely added to the system. Now
+features that are unlikely to be of general use are encouraged to live in their
+own projects that import and extend `music21`.
+
## Resources ##
@@ -14,6 +19,17 @@ documentation improvements. The
[Code of Conduct](README.md)
+## Preparing to Contribute ##
+
+Be sure to install all of the requirements in `requirements_dev.txt` via:
+
+```
+pip3 install -r requirements_dev.txt
+```
+
+There are several tools needed for fully testing music21 that aren't included
+in the standard `requirements.txt`.
+
## Issue Tickets ##
Please use the provided templates for bug reports or feature proposals. For issues
@@ -35,7 +51,8 @@ or Stack Overflow.
## Submitting Pull Requests ##
Open an issue to propose a feature or report a bug before raising a pull request.
-You can include a diff or link to your own repo if you've already started some of the work.
+You can include a diff or link to your own repo if you've
+already started some of the work.
(Changes where the motivation is self-evident, like handling exceptions or increasing
test coverage, don't need issue tickets.)
@@ -55,36 +72,88 @@ between reviews. (Squashing commits will be handled by the merger.)
## Style Guide ##
-`music21` began in Perl. Now it interfaces with the TypeScript package `music21j`.
-For these reasons, as well as for backward compatibility, we adhere to `camelCase`
-naming--rather than PEP8 `snake_case` naming--for public APIs such as method names,
+`Music21` began in Perl. Now it interfaces with the TypeScript package `music21j`.
+Both of those languages use `camelCase` extensively. For these reasons,
+as well as for backward compatibility, we adhere to `camelCase`
+naming--rather than Python PEP8 `snake_case` naming--for public APIs such as method names,
arguments, and the like.
That said, snake_case is acceptable (even encouraged!) in new contributions
for internal variables that are not exposed publicly. Screen readers can
-pronounce these names better.
+pronounce these names better and make them more accessible.
+However, all exposed methods and functions and their exposed arguments must
+be in camelCase.
Conventions:
- - `'strings MUST be single-quoted, but "double quotes" are allowed internally'`
+ - **strings MUST be 'single-quoted', but "double quotes" are allowed internally**
+ - this rule applies to triple quotes and doc strings also, contrary to PEP 257.
+ - Docstrings must begin and end on their own lines. No one-line doc-strings or text
+ immediately following the triple quotes.
+ - when there is a hyphen or single quote in the string, double quotes should be used,
+ not escaping/backslashing.
+ - For long streams of TinyNotation or Lilypond code, which both use single quotes to indicate octave,
+ triple single quotes around the string are better than double quotes. Internal whitespace
+ rarely matters in those formats.
+ - Documentation should follow quoting in American English grammar when not
+ discussing code. So for instance, a quotation in documentation is in double quotes.
- variable names:
- - need to be unambiguous
- - must begin with lowercase
- - snake_case is encouraged for private variables
- - line lengths are capped at 100, but if approaching this limit, look for ways to avoid one-lining
- - line continuation characters (`\`) are not allowed; use open parentheses
- - prefer f-strings to `%` or `.format()`
- - annotating types is encouraged: e.g. `self.counter: int = 0` or `def makeNoises() -> List['noise.Noise']:`
+ - need to be unambiguous, even if rather long.
+ - must begin with lowercase (or underscore + lowercase) unless they represent classes.
+ - snake_case is allowed/encouraged for private variables for accessibility.
+ - camelCase is required for public.
+ - line lengths are capped at 100, but if approaching this limit, look for ways to avoid one-lining.
+ - if it's easy to split your line into two which are both under 80 characters, do so.
+ - line continuation characters (`\`) are not allowed; use open parentheses.
+ - prefer f-strings to `.format()`. The `%` interpolation is no longer allowed.
+ - annotating types is required in new code, and encouraged to be added to older code.
+ - e.g. `self.counter: int = 0` or `def makeNoises() -> list['noise.Noise']:`
+ - The typing library should always be imported as `t`.
+ - Until `music21` no longer supports Python 3.9, use `t.Optional[x]` rather than `x|None`.
- prefer enums to string configurations
+ - in music21.common.enums, there is a StrEnum class for nearly-backwards compatible
+ replacement of a former string argument with an enum.
+ - new Enums do not need to inherit from StrEnum or IntEnum.
+ - Enum members should be in ALL_CAPS with underscores, unless there is a good reason (such
+ as case differentiation) to use CamelCase. I personally hate this convention--it looks
+ like shouting--and will revisit it if `music21` is ever ported to a language where
+ this is not the convention. Do not leave out underscores in the member names; it makes
+ it hard for people whose native language is not English to parse and impossible for
+ screen readers.
- methods:
- - no more than three positional arguments (in addition to `self`)
+ - no more than three positional arguments (four, if the first is `self`)
- keyword arguments should be keyword-only by using `*`
- to consume any other positional arguments: `def makeNoise(self, volume, *, color=noise.Pink):`
- - avoid generic `**kwargs`; make keywords explicit
+ to consume any other positional arguments: `def makeNoise(self, volume, *, color=noise.PINK):`
+ - avoid generic `**keywords`; make keywords explicit.
+ (This rule does not necessarily apply for subclass inheritance where you want to allow the superclass
+ to add more features later. But see the Liskov principle next.)
+ See also https://github.com/cuthbertLab/music21/issues/1389
+ - prefer methods that by default do not alter the object passed in and instead return a new one.
+ It is permitted and encouraged to have an `inPlace: bool = False` argument that allows for
+ manipulation of the original object. When `inPlace` is True, nothing should be returned
+ (not true for `music21j` since passing through objects is so expected in JavaScript thanks
+ to JQuery and other libraries). Use the `@overload` decorator to show how this parameter
+ affects the return value.
+ - New classes should strongly endeavor to follow Liskov Substitution Principle.
+ - Exceptions may be granted if the class structures follow names that are in common musical use
+ but whose real world objects do not follow this principle. For instance, a `Manx` is a subclass
+ of `Cat` without `self.tail`. Sometimes, however, rewriting the superclass might be possible
+ (perhaps `self.tail: t.Optional[Tail]`).
+ - `Music21` was originally designed without this principle in mind, so you will find
+ parts of the system that do not follow LSP and for backwards compatibility never will.
+ I (Myke) have personally apologized to Barbara Liskov for my past ignorance.
- use [Sphinx formatting](https://web.mit.edu/music21/doc/developerReference/documenting.html#documenting-modules-and-classes)
to link to classes and methods in docstrings
- use descriptive pull request titles (rather than GitHub's default "Update pitch.py")
-
+ - do not have the PR title too long that it cannot be seen in one line. Simplify and
+ rewrite and go into more detail in the description. I depend on skimming PR titles
+ to prepare reports of changes.
+ - don't turn a PR into a general discussion of something not included in the PR.
+ Make an issue and link to the PR from there.
+ - Write the word `music21` in lowercase in the middle of a sentence and as `Music21` at
+ the beginning of a sentence. Avoid beginning a sentence with a module name, but if
+ it has to be done, write it in lowercase, obviously. In contexts where possible,
+ `music21` should be stylized in monospaced (with slab serifs, like old typewriters).
## Testing ##
@@ -97,16 +166,20 @@ browser windows or playing music).
Pull requests that increase or improve coverage of existing features are very welcome.
Coverage reports can be found at [Coveralls](https://coveralls.io/github/cuthbertLab/music21).
+Pull requests that lower overall coverage are likely to be rejected (exception: replace
+30 covered lines with 5 covered lines that do the same job more efficiently, and you've
+lowered the overall coverage, but that's okay).
For changes to file parsing, please test both import and export (when supported for
that format), and please increment the most minor version number in `music21.__version__`
-so that cached files will be invalidated. Your tests can use `converter.parse(fp, forceSource=True)`
-so that tests have a chance to fail locally, but in most cases we will ask you to
-remove this keyword when polishing the patch.
+so that cached files will be invalidated. When writing a PR that changes imports, feel
+free to use `converter.parse(fp, forceSource=True)` so that tests have a chance
+to fail locally, but in most cases we will ask you to remove this keyword
+when polishing the patch.
## Finally ##
If you're looking for ways to get started, browse the issues board, the Coveralls module
coverage, or the TODO stubs in an area of the code that interests you.
-Thanks for your interest in contributing to music21. We look forward to seeing your work!
+Thanks for your interest in contributing to `music21`. We look forward to seeing your work!
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index da67e8986d..0000000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,7 +0,0 @@
-global-include *.p.gz *.py *.txt *.xml *.krn *.mxl *.musicxml *.pdf *.html *.ipynb *.css *.js *.png *.jpg *.mid *.abc *.json *.md *.rst *.rntxt *.command *.scl *nwc *.nwctxt *.wav *.mei LICENSE
-global-exclude *ipynb_checkpoints* *-checkpoint.ipynb
-prune dist
-prune .mypy_cache
-prune music21/.mypy_cache
-prune obsolete
-prune documentation
diff --git a/README.md b/README.md
index 39cd40ce17..da5a7f387f 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,28 @@
# music21 #
-music21 -- A Toolkit for Computational Musicology
+`music21` -- A Toolkit for Computer-Aided Musical Analysis and
+Computational Musicology
-Copyright © 2006-2021, Michael Scott Cuthbert and cuthbertLab
+Copyright © 2006-2023 [Michael Scott Asato Cuthbert](http://www.trecento.com)
For more information, visit:
https://web.mit.edu/music21
+To try it out, visit:
+https://tinyurl.com/m21colab (runs music21 v7)
+
And to install, see:
https://web.mit.edu/music21/doc/usersGuide/usersGuide_01_installing.html
-Music21 runs on Python 3.7+. Use version 4 on Python 2 or Py3.4, version 5
-on Py3.5, version 6 on Py3.6.
+`Music21` runs on Python 3.10+. Use version 4 on Python 2 or Py3.4, version 5
+on Py3.5, version 6 on Py3.6, version 7 on Py3.7, version 8 on Py3.8/Py3.9.
-Released under the BSD (3-clause) license. Music21 may also be used
-under the LGPL license. See LICENSE.
+Released under the BSD (3-clause) license. See LICENSE.
Externally provided software (including the MIT licensed Lilypond/MusicXML test Suite) and
-music encoding in the corpus may have different licenses. A no-corpus version of music21
-is available also on GitHub.
+music encoding in the corpus may have different licenses and/or copyrights.
+A no-corpus version of `music21` is available also on GitHub.
[![Build Status](https://github.com/cuthbertLab/music21/workflows/maincheck/badge.svg)](https://github.com/cuthbertLab/music21)
-[![Lint Status](https://github.com/cuthbertLab/music21/workflows/PyLint/badge.svg)](https://github.com/cuthbertLab/music21)
[![Coverage Status](https://coveralls.io/repos/github/cuthbertLab/music21/badge.svg?branch=master)](https://coveralls.io/github/cuthbertLab/music21?branch=master)
## Documentation ##
@@ -39,7 +41,7 @@ See: https://groups.google.com/forum/#!forum/music21list
## Community Code of Conduct ##
-Music21 encourages contributions, discussions, and usage from all people interested in
+`Music21` encourages contributions, discussions, and usage from all people interested in
music and computers. This encouragement extends to all people regardless of (among other aspects)
gender, race, sexual orientation, disability, religion, appearance, veteran status,
gender identity, socioeconomic status, or nationality.
@@ -54,7 +56,16 @@ sometimes make mistakes and will, in general, accept sincere regrets for such ca
Blatant or repeated violations of the code will result in the removal of the
contributor’s participation in the community.
-The maintainers of music21 and associated sites will commit themselves to enforcing
+The maintainers of `music21` and associated sites will commit themselves to enforcing
this code of conduct. Users who notice violations, including instances of abuse,
-harassment, or otherwise unacceptable behavior are requested to contact cuthbert@mit.edu.
+harassment, or otherwise unacceptable behavior are requested to contact
+michael.asato.cuthbert@gmail.com.
Maintainers will respect confidentiality with regard to reports.
+
+## Acknowledgements ##
+
+The development of `music21` has been supported by
+the generosity of the Seaver Institute and the
+National Endowment for the Humanities. MIT's Music and Theater Arts Section
+and the School of Humanities, Arts, and Social Sciences encouraged Cuthbert
+in its development.
diff --git a/dist/dist.py b/dist/dist.py
index 966b07d179..639db41214 100644
--- a/dist/dist.py
+++ b/dist/dist.py
@@ -4,9 +4,9 @@
# Purpose: Distribution and uploading script
#
# Authors: Christopher Ariza
-# Michael Scott Cuthbert
+# Michael Scott Asato Cuthbert
#
-# Copyright: Copyright © 2010-2021 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2010-2023 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
@@ -17,11 +17,11 @@
1. update the VERSION in _version.py and the single test cases in base.py.
2. run `corpus.corpora.CoreCorpus().cacheMetadata()`.
for a major change run corpus.corpora.CoreCorpus().rebuildMetadataCache()
- (40 min on MacPro) -- either of these MAY change a lot of tests in corpus, metadata, etc.
- so don't skip the next step!
+ (20 min on IntelMacbook Air) -- either of these MAY change a
+ lot of tests in corpus, metadata, etc. so don't skip the next step!
3. run test/warningMultiprocessTest.py for lowest and highest Py version -- fix all warnings!
4. run test/testLint.py and fix any lint errors (covered now by CI)
-5. commit and then check test/testSingleCoreAll.py or wait for results on Github Actions
+5. commit and then check test/testSingleCoreAll.py or wait for results on GitHub Actions
(normally not necessary, because it's slower and mostly duplicates multiprocessTest,
but should be done before making a release).
6. IMPORTANT: run python documentation/testDocumentation.py and afterwards fix errors [*]
@@ -35,7 +35,7 @@
8. run documentation/make.py linkcheck [*]
9. run documentation/make.py [*]
-[*] you will need sphinx, IPython (pip or easy_install), markdown, and pandoc (.dmg) installed
+[*] you will need sphinx, Jupyter (pip or easy_install), markdown, and pandoc (.dmg) installed
10. ssh to athena.dialup.mit.edu (yes, dialup!), cd music21/doc and rm -rf * (skip on minor version changes)
@@ -45,25 +45,28 @@
12. zip up documentation/build/html and get ready to upload/delete it.
Rename to music21.v.7.1.0-docs.zip (skip for Alpha/Beta)
-12b. If any new file extensions have been added, be sure to add them to MANIFEST.in
+13. Run "hatch build" -- requires hatch to be installed "brew install hatch"
-13. And finally this file. (from the command line; not as python -m...)
- There are major problems in SetupTools -- v8 (or even a 7.3.1) needs to
- fix them -- creating a dir music21.egg-info in the main dir with a
- requires.txt file created as root.
+14. Run this file -- it builds the no-corpus version of music21.
+ DO NOT RUN THIS ON A PC -- the Mac .tar.gz might have an incorrect permission if you do.
-14. COMMIT to Github at this point w/ commit comment of the new version,
+15. COMMIT to GitHub at this point w/ commit comment of the new version,
then don't change anything until the next step is done.
(.gitignore will avoid uploading the large files created here...)
-15. Create a new release on GitHub and upload the TWO files created here and docs.
- Use tag v7.3.1 (etc.).
+16. Tag the commit: git tag -a vX.Y.Z -m "music21 vX.Y.Z"
Don't forget the "v" in the release tag.
+ Sanity check that the correct commit was tagged: git log
+
+17. Push tags: git push --tags (or git push upstream --tags if not on main branch)
+
+18. Create a new release on GitHub and upload the TWO non-wheel files created here and docs.
Drag in this order: .tar.gz, documentation, no-corpus.tar.gz
Finish this before doing the next step, even though it looks like it could be done in parallel.
-16. Upload the new file to PyPI with "twine upload music21-7.3.5a2.tar.gz" [*]
+19. Upload the new file to PyPI with "twine upload music21-7.3.5a2.tar.gz", and same for the
+ whl file (but NOT no corpus) [*]
[*] Requires twine to be installed
@@ -77,251 +80,68 @@
username:your_username
password:your_password
-17. Delete the two .tar.gz files in dist...
+20. Delete the two .tar.gz files and .whl file in dist...
-18. For starting a new major release create a GitHub branch for the old one.
+21. For starting a new major release create a GitHub branch for the old one.
-19. Immediately increment the number in _version.py and run tests on it here
+22. Immediately increment the number in _version.py and run tests on it here
to prepare for next release.
-20. Announce on the blog, to the list, and twitter.
-
-DO NOT RUN THIS ON A PC -- the Mac .tar.gz has an incorrect permission if you do.
+23. Announce on the blog, to the list, and twitter.
'''
-import hashlib
import os
-import sys
import shutil
import tarfile
-from music21 import base
-from music21 import common
-
-from music21 import environment
-environLocal = environment.Environment('..dist.dist')
-
-PY = sys.executable
-environLocal.warn(f'using python executable at {PY}')
-
-class Distributor:
- def __init__(self):
- # self.fpEgg = None
- # self.fpWin = None
- self.fpTar = None
-
- self.buildNoCorpus = True
- # self.fpEggNoCorpus = None
- self.fpTarNoCorpus = None
-
- self.version = base.VERSION_STR
-
- self._initPaths()
-
- def _initPaths(self):
-
- # must be in the dist dir
- directory = os.getcwd()
- parentDir = os.path.dirname(directory)
- parentContents = sorted(os.listdir(parentDir))
- # make sure we are in the proper directory
- if (not directory.endswith('dist')
- or 'music21' not in parentContents):
- raise Exception(f'not in the music21{os.sep}dist directory: {directory}')
-
- self.fpDistDir = directory
- self.fpPackageDir = parentDir # dir with setup.py
- self.fpBuildDir = os.path.join(self.fpPackageDir, 'build')
- # self.fpEggInfo = os.path.join(self.fpPackageDir, 'music21.egg-info')
-
- sys.path.insert(0, parentDir) # to get setup in as a possibility.
-
- for fp in [self.fpDistDir, self.fpPackageDir, self.fpBuildDir]:
- environLocal.warn(fp)
-
-
- def updatePaths(self):
- '''
- Process output of build scripts. Get most recently produced distributions.
- '''
- contents = sorted(os.listdir(self.fpDistDir))
- for fn in contents:
- fp = os.path.join(self.fpDistDir, fn)
- # if self.version in fn and fn.endswith('.egg'):
- # self.fpEgg = fp
- # if self.version in fn and fn.endswith('.exe'):
- # fpNew = fp.replace('.macosx-10.8-intel.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.8-x86_64.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.9-intel.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.9-x86_64.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.10-intel.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.10-x86_64.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.11-intel.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.11-x86_64.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.12-intel.exe', '.win32.exe')
- # fpNew = fpNew.replace('.macosx-10.12-x86_64.exe', '.win32.exe')
- # if fpNew != fp:
- # os.rename(fp, fpNew)
- # self.fpWin = fpNew
-
- print(fn)
- if self.version in fn and fn.endswith('.tar.gz'):
- self.fpTar = fp
- else:
- environLocal.warn(fn + ' does not end with .tar.gz')
-
- environLocal.warn('giving path for tar.gz')
- for fn in [self.fpTar]:
- if fn is None:
- environLocal.warn('missing fn path')
- else:
- environLocal.warn(fn)
-
- def removeCorpus(self, fp):
- '''
- Remove the corpus from a compressed file (.tar.gz) and
- create a new music21-noCorpus version.
-
- Return the completed file path of the newly created edition.
-
- NOTE: this function works only with Posix systems.
- '''
- TAR = 'TAR'
- # EGG = 'EGG'
- if fp and fp.endswith('.tar.gz'):
- mode = TAR
- modeExt = '.tar.gz'
- else:
- raise Exception('incorrect source file path')
-
- fpDir, fn = os.path.split(fp)
-
- # this has .tar.gz extension; this is the final completed package
- fnDst = fn.replace('music21', 'music21-noCorpus')
- fpDst = os.path.join(fpDir, fnDst)
- # remove file extensions
- fnDstDir = fnDst.replace(modeExt, '')
- fpDstDir = os.path.join(fpDir, fnDstDir)
-
- # get the name of the dir after decompression
- fpSrcDir = os.path.join(fpDir, fn.replace(modeExt, ''))
-
- # remove old dirs if it exists
- if os.path.exists(fpDst):
- shutil.rmtree(fpDst)
-
- if os.path.exists(fpDstDir):
- shutil.rmtree(fpDstDir)
-
- if mode == TAR:
- tf = tarfile.open(fp, "r:gz")
- # the path here is the dir into which to expand,
- # not the name of that dir
- tf.extractall(path=fpDir)
- os.system(f'mv {fpSrcDir} {fpDstDir}')
- tf.close() # done after extraction
-
- # elif mode == EGG:
- # os.system(f'mkdir {fpDstDir}')
- # # need to create dst dir to unzip into
- # tf = zipfile.ZipFile(fp, 'r')
- # tf.extractall(path=fpDstDir)
-
-
- # remove files, updates manifest
- for fn in common.getCorpusContentDirs():
- fp = os.path.join(fpDstDir, 'music21', 'corpus', fn)
- shutil.rmtree(fp)
-
- fp = os.path.join(fpDstDir, 'music21', 'corpus', '_metadataCache')
+from music21._version import __version__ as version
+from music21.common.pathTools import getRootFilePath, getCorpusContentDirs
+
+def removeCorpus():
+ '''
+ Remove the corpus from a compressed file (.tar.gz) and
+ create a new music21-noCorpus version.
+
+ Return the completed file path of the newly created edition.
+
+ NOTE: this function works only with Posix systems.
+ '''
+ fp = getRootFilePath() / 'dist' / ('music21-' + version + '.tar.gz')
+ fpDir, fn = os.path.split(str(fp))
+
+ # this has .tar.gz extension; this is the final completed package
+ fnDst = fn.replace('music21', 'music21-noCorpus')
+ fpDst = os.path.join(fpDir, fnDst)
+ # remove file extensions
+ fnDstDir = fnDst.replace('.tar.gz', '')
+ fpDstDir = os.path.join(fpDir, fnDstDir)
+
+ file = tarfile.open(fp)
+ file.extractall(fpDir)
+ file.close()
+
+ os.rename(fpDstDir.replace('-noCorpus', ''), fpDstDir)
+
+ # remove files, updates manifest
+ for fn in getCorpusContentDirs():
+ fp = os.path.join(fpDstDir, 'music21', 'corpus', fn)
shutil.rmtree(fp)
- # adjust the sources Txt file
- # if mode == TAR:
- sourcesTxt = os.path.join(fpDstDir, 'music21.egg-info', 'SOURCES.txt')
- # else:
- # raise Exception('invalid mode')
-
- # elif mode == EGG:
- # sourcesTxt = os.path.join(fpDstDir, 'EGG-INFO', 'SOURCES.txt')
-
- # files will look like 'music21/corpus/haydn' in SOURCES.txt
- post = []
- f = open(sourcesTxt, 'r')
- corpusContentDirs = common.getCorpusContentDirs()
- for line in f:
- match = False
- if 'corpus' in line:
- for fn in corpusContentDirs:
- # these are relative paths
- fp = os.path.join('music21', 'corpus', fn)
- if line.startswith(fp):
- match = True
- break
- if not match:
- post.append(line)
- f.close()
- f = open(sourcesTxt, 'w')
- f.writelines(post)
- f.close()
-
- if mode == TAR:
- # compress dst dir to dst file path name
- # need the -C flag to set relative dir
- # just name of dir
- cmd = f'tar -C {fpDir} -czf {fpDst} {fnDstDir}/'
- os.system(cmd)
-
- # remove directory that was compressed
- if os.path.exists(fpDstDir):
- shutil.rmtree(fpDstDir)
-
- return fpDst # full path with extension
-
-
-
- def build(self):
- '''
- Build all distributions. Update and rename file paths if necessary;
- remove extract build products.
- '''
- # call setup.py
- # import setup # -- for some reason does not work unless called from command line
- for buildType in ['sdist --formats=gztar', 'bdist_wheel']:
- environLocal.warn(f'making {buildType}')
-
- savePath = os.getcwd()
- os.chdir(self.fpPackageDir)
- os.system(f'{PY} setup.py {buildType}')
- os.chdir(savePath)
-
- self.updatePaths()
-
- environLocal.warn(f'removing {self.fpBuildDir} (except on windows...there do it yourself)')
- try:
- shutil.rmtree(self.fpBuildDir)
- except FileNotFoundError:
- environLocal.warn(
- 'Directory was already cleaned up'
- )
-
- if self.buildNoCorpus is True:
- # create no corpus versions
- self.fpTarNoCorpus = self.removeCorpus(fp=self.fpTar)
- # self.fpEggNoCorpus = self.removeCorpus(fp=self.fpEgg)
-
-
- def md5ForFile(self, path, hexReturn=True):
- if hexReturn:
- return hashlib.md5(open(path, 'rb').read()).hexdigest()
- else:
- return hashlib.md5(open(path, 'rb').read()).digest()
+ fp = os.path.join(fpDstDir, 'music21', 'corpus', '_metadataCache')
+ shutil.rmtree(fp)
+
+ # compress dst dir to dst file path name
+ # need the -C flag to set relative dir
+ # just name of dir
+ cmd = f'tar -C {fpDir} -czf {fpDst} {fnDstDir}/'
+ os.system(cmd)
+
+ # # remove directory that was compressed
+ if os.path.exists(fpDstDir):
+ shutil.rmtree(fpDstDir)
+
+ return fpDst # full path with extension
# ------------------------------------------------------------------------------
if __name__ == '__main__':
- d = Distributor()
- d.buildNoCorpus = True
- d.build()
- d.updatePaths()
- # d.getMD5Path()
- # d.uploadPyPi()
+ removeCorpus()
diff --git a/documentation/autogenerated/index.rst b/documentation/autogenerated/index.rst
index e6f8502492..bce4d750b9 100644
--- a/documentation/autogenerated/index.rst
+++ b/documentation/autogenerated/index.rst
@@ -27,3 +27,8 @@
:maxdepth: 2
developerReference/index
+
+.. toctree::
+ :maxdepth: 1
+
+ testsAndInProgress/index
\ No newline at end of file
diff --git a/documentation/docbuild/documenters.py b/documentation/docbuild/documenters.py
index 1e15c07454..239bf71869 100644
--- a/documentation/docbuild/documenters.py
+++ b/documentation/docbuild/documenters.py
@@ -5,16 +5,17 @@
#
# Authors: Josiah Wolf Oberholtzer
# Christopher Ariza
-# Michael Scott Cuthbert
+# Michael Scott Asato Cuthbert
#
-# Copyright: Copyright © 2013-17 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2013-17 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
+from __future__ import annotations
+import builtins
import inspect
import re
import types
-from typing import List
import unittest
from music21 import common
@@ -79,10 +80,15 @@ def makeRubric(text):
class ObjectDocumenter(Documenter):
'''
- Base class for object documenting sub-classes. such as ClassDocumenter
+ Base class for object documenting subclasses. such as ClassDocumenter
'''
- _DOC_ATTR = {'referent': 'the object being documented'}
+ _DOC_ATTR: dict[str, str] = {
+ 'referent':
+ ''''
+ The object being documented.
+ ''',
+ }
# INITIALIZER #
sphinxCrossReferenceRole = ''
@@ -97,7 +103,7 @@ def referentPackageSystemPath(self):
return ''
@property
- def rstAutodocDirectiveFormat(self) -> List[str]:
+ def rstAutodocDirectiveFormat(self) -> list[str]:
return []
@property
@@ -168,7 +174,7 @@ def referentPackageSystemPath(self):
return path.replace('.__init__', '')
@property
- def rstAutodocDirectiveFormat(self) -> List[str]:
+ def rstAutodocDirectiveFormat(self) -> list[str]:
'''
>>> function = common.opFrac
>>> documenter = FunctionDocumenter(function)
@@ -187,17 +193,17 @@ class MemberDocumenter(ObjectDocumenter):
'''
Abstract base class for documenting class members such as Methods and Attributes and Properties
'''
- _DOC_ATTR = {
+ _DOC_ATTR: dict[str, str] = {
'memberName': 'the short name of the member, for instance "mode"',
- 'referent': '''the attribute or method itself, such as (no quotes)
- key.KeySignature.mode''',
- 'definingClass': '''the class the referent belongs to, such as (no quotes)
- key.KeySignature''',
+ 'referent': '''the attribute or method itself, such as
+ ''',
+ 'definingClass': '''the class the referent belongs to, such as
+ ''',
}
# INITIALIZER #
- def __init__(self, referent, memberName, definingClass):
+ def __init__(self, referent, memberName: str, definingClass: type):
if not isinstance(definingClass, type):
raise Music21Exception(f'referent must be a class, not {referent}')
super().__init__(referent)
@@ -265,7 +271,12 @@ class MethodDocumenter(MemberDocumenter):
@property
def rstAutodocDirectiveFormat(self):
result = []
- result.append(f'.. automethod:: {self.referentPackageSystemPath}')
+ indent = ''
+ if getattr(self.referent, '_isDeprecated', False):
+ indent = ' '
+ result.append('.. cssclass:: strike')
+ result.append('')
+ result.append(f'{indent}.. automethod:: {self.referentPackageSystemPath}')
result.append('')
return result
@@ -386,8 +397,8 @@ class ClassDocumenter(ObjectDocumenter):
:columns: 3
- :attr:`~music21.base.Music21Object.classSortOrder`
- :attr:`~music21.base.Music21Object.groups`
- - :attr:`~music21.base.Music21Object.id`
- :attr:`~music21.base.Music21Object.isStream`
+ - :attr:`~music21.base.Music21Object.sites`
'''
# CLASS VARIABLES #
@@ -433,7 +444,7 @@ def findAttributes(self):
find all attributes in self.referent and set classes appropriately.
'''
- attrs = inspect.classify_class_attrs(self.referent)
+ attrs: list[inspect.Attribute] = inspect.classify_class_attrs(self.referent)
for attr in attrs:
self.findOneAttribute(attr)
@@ -455,9 +466,9 @@ def sortMemberLists(self):
documenters.sort(key=keyLambda)
- def findOneAttribute(self, attr):
+ def findOneAttribute(self, attr: inspect.Attribute):
# Ignore definitions derived directly from builtins (object, list, etc.)
- if attr.defining_class in __builtins__.values():
+ if attr.defining_class in builtins.__dict__.values():
return
# Ignore private members ('_') and special members ('__')
elif attr.name.startswith('_') and attr.name not in self._allowedSpecialMethods:
@@ -637,7 +648,7 @@ def inheritedDocAttrMapping(self):
...
-
+
'''
# if one of the _DOC_ATTRs is exactly the same as the previous base class,
# only show it once.
@@ -680,19 +691,19 @@ def inheritedReadonlyPropertiesMapping(self):
music21.prebase.ProtoM21Object:
- music21.prebase.ProtoM21Object.classSet
- music21.prebase.ProtoM21Object.classes
- music21.stream.Stream:
- - music21.stream.Stream.beat
- - music21.stream.Stream.beatDuration
- - music21.stream.Stream.beatStr
- - music21.stream.Stream.beatStrength
- - music21.stream.Stream.flat
- - music21.stream.Stream.highestOffset
- - music21.stream.Stream.highestTime
- - music21.stream.Stream.isGapless
- - music21.stream.Stream.iter
- - music21.stream.Stream.lowestOffset
- music21.stream.core.StreamCoreMixin:
- - music21.stream.core.StreamCoreMixin.spannerBundle
+ music21.stream.base.Stream:
+ - music21.stream.base.Stream.beat
+ - music21.stream.base.Stream.beatDuration
+ - music21.stream.base.Stream.beatStr
+ - music21.stream.base.Stream.beatStrength
+ - music21.stream.base.Stream.flat
+ - music21.stream.base.Stream.highestOffset
+ - music21.stream.base.Stream.highestTime
+ - music21.stream.base.Stream.isGapless
+ - music21.stream.base.Stream.lowestOffset
+ - music21.stream.base.Stream.notes
+ music21.stream.core.StreamCore:
+ - music21.stream.core.StreamCore.spannerBundle
'''
return self._inheritedReadonlyPropertiesMapping
@@ -708,11 +719,9 @@ def inheritedMethodsMapping(self):
>>> mapping = documenter.inheritedMethodsMapping
>>> sortBy = lambda x: x.referentPackageSystemPath
>>> for classDocumenter in sorted(mapping, key=sortBy):
- ... print('{0}:'.format(
- ... classDocumenter.referentPackageSystemPath))
+ ... print(f'{classDocumenter.referentPackageSystemPath}:')
... for attributeDocumenter in mapping[classDocumenter][:5]:
- ... print('- {0}'.format(
- ... attributeDocumenter.referentPackageSystemPath))
+ ... print(f'- {attributeDocumenter.referentPackageSystemPath}')
...
music21.base.Music21Object:
- music21.base.Music21Object.clearCache
@@ -722,18 +731,18 @@ def inheritedMethodsMapping(self):
- music21.base.Music21Object.getContextByClass
music21.prebase.ProtoM21Object:
- music21.prebase.ProtoM21Object.isClassOrSubclass
- music21.stream.Stream:
- - music21.stream.Stream.activateVariants
- - music21.stream.Stream.addGroupForElements
- - music21.stream.Stream.allPlayingWhileSounding
- - music21.stream.Stream.analyze
- - music21.stream.Stream.append
- music21.stream.core.StreamCoreMixin:
- - music21.stream.core.StreamCoreMixin.asTimespans
- - music21.stream.core.StreamCoreMixin.asTree
- - music21.stream.core.StreamCoreMixin.coreAppend
- - music21.stream.core.StreamCoreMixin.coreElementsChanged
- - music21.stream.core.StreamCoreMixin.coreGatherMissingSpanners
+ music21.stream.base.Stream:
+ - music21.stream.base.Stream.__getitem__
+ - music21.stream.base.Stream.activateVariants
+ - music21.stream.base.Stream.addGroupForElements
+ - music21.stream.base.Stream.allPlayingWhileSounding
+ - music21.stream.base.Stream.analyze
+ music21.stream.core.StreamCore:
+ - music21.stream.core.StreamCore.asTimespans
+ - music21.stream.core.StreamCore.asTree
+ - music21.stream.core.StreamCore.coreAppend
+ - music21.stream.core.StreamCore.coreCopyAsDerivation
+ - music21.stream.core.StreamCore.coreElementsChanged
'''
return self._inheritedMethodsMapping
@@ -757,20 +766,16 @@ def inheritedReadwritePropertiesMapping(self):
- music21.base.Music21Object.activeSite
- music21.base.Music21Object.derivation
- music21.base.Music21Object.editorial
- - music21.base.Music21Object.offset
- - music21.base.Music21Object.priority
- - music21.base.Music21Object.quarterLength
+ - music21.base.Music21Object.id
+ ...
- music21.base.Music21Object.style
- music21.stream.Stream:
- - music21.stream.Stream.atSoundingPitch
- - music21.stream.Stream.clef
- - music21.stream.Stream.duration
- - music21.stream.Stream.elements
- - music21.stream.Stream.finalBarline
- - music21.stream.Stream.keySignature
- - music21.stream.Stream.metadata
- - music21.stream.Stream.seconds
- - music21.stream.Stream.timeSignature
+ music21.stream.base.Stream:
+ - music21.stream.base.Stream.atSoundingPitch
+ - music21.stream.base.Stream.clef
+ ...
+ - music21.stream.base.Stream.seconds
+ - music21.stream.base.Stream.staffLines
+ - music21.stream.base.Stream.timeSignature
'''
return self._inheritedReadwritePropertiesMapping
@@ -787,25 +792,25 @@ def methods(self):
... method
...
+ music21.stream.base.Stream.__getitem__>
+ music21.stream.base.Stream.activateVariants>
+ music21.stream.base.Stream.addGroupForElements>
+ music21.stream.base.Stream.allPlayingWhileSounding>
+ music21.stream.base.Stream.analyze>
+ music21.stream.base.Stream.append>
+ music21.stream.base.Stream.attachIntervalsBetweenStreams>
+ music21.stream.base.Stream.attachMelodicIntervals>
+ music21.stream.base.Stream.augmentOrDiminish>
+ music21.stream.base.Stream.beatAndMeasureFromOffset>
'''
return self._methods
@@ -820,26 +825,12 @@ def readonlyProperties(self):
>>> for attr in documenter.readonlyProperties:
... attr
...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ ...
'''
return self._readonlyProperties
@@ -854,17 +845,16 @@ def readwriteProperties(self):
>>> for prop in documenter.readwriteProperties:
... prop
...
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
'''
return self._readwriteProperties
@@ -978,6 +968,7 @@ def rstInheritedMethodsFormat(self):
'.. hlist::'
' :columns: 3'
''
+ ' - :meth:`~music21.note.NotRest.getInstrument`'
' - :meth:`~music21.note.NotRest.hasVolumeInformation`'
''
'Methods inherited from :class:`~music21.note.GeneralNote`:'
@@ -998,7 +989,6 @@ def rstInheritedMethodsFormat(self):
' - :meth:`~music21.base.Music21Object.clearCache`'
' - :meth:`~music21.base.Music21Object.containerHierarchy`'
' - :meth:`~music21.base.Music21Object.contextSites`'
- ' - :meth:`~music21.base.Music21Object.getAllContextsByClass`'
...
''
@@ -1071,6 +1061,8 @@ def rstInheritedReadwritePropertiesFormat(self):
' :columns: 3'
''
' - :attr:`~music21.note.GeneralNote.lyric`'
+ ' - :attr:`~music21.note.GeneralNote.pitches`'
+ ' - :attr:`~music21.note.GeneralNote.tie`'
''
'Read/write properties inherited from :class:`~music21.base.Music21Object`:'
''
@@ -1081,6 +1073,7 @@ def rstInheritedReadwritePropertiesFormat(self):
' - :attr:`~music21.base.Music21Object.derivation`'
' - :attr:`~music21.base.Music21Object.duration`'
' - :attr:`~music21.base.Music21Object.editorial`'
+ ' - :attr:`~music21.base.Music21Object.id`'
' - :attr:`~music21.base.Music21Object.offset`'
' - :attr:`~music21.base.Music21Object.priority`'
' - :attr:`~music21.base.Music21Object.quarterLength`'
@@ -1118,6 +1111,7 @@ def rstMethodsFormat(self):
'.. hlist::'
' :columns: 3'
''
+ ' - :meth:`~music21.scale.ConcreteScale.__eq__`'
' - :meth:`~music21.scale.ConcreteScale.derive`'
' - :meth:`~music21.scale.ConcreteScale.deriveAll`'
...
@@ -1238,6 +1232,7 @@ def rstReadwritePropertiesFormat(self):
' - :attr:`~music21.base.Music21Object.derivation`'
' - :attr:`~music21.base.Music21Object.duration`'
' - :attr:`~music21.base.Music21Object.editorial`'
+ ' - :attr:`~music21.base.Music21Object.id`'
' - :attr:`~music21.base.Music21Object.offset`'
' - :attr:`~music21.base.Music21Object.priority`'
' - :attr:`~music21.base.Music21Object.quarterLength`'
@@ -1391,7 +1386,7 @@ def classDocumenters(self):
for referent in self.memberOrder:
if referent in classDocumenters:
result.append(classDocumenters[referent])
- del(classDocumenters[referent])
+ del classDocumenters[referent]
for documenter in sorted(
classDocumenters.values(),
key=lambda x: x.referentPackageSystemPath):
@@ -1411,10 +1406,9 @@ def functionDocumenters(self):
>>> for functionDocumenter in modDocumenter.functionDocumenters:
... print(functionDocumenter.referentPackageSystemPath)
...
- music21.serial.getHistoricalRowByName
music21.serial.pcToToneRow
music21.serial.rowToMatrix
-
+ music21.serial.getHistoricalRowByName
'''
result = []
functionDocumenters = {}
@@ -1424,7 +1418,7 @@ def functionDocumenters(self):
for referent in self.memberOrder:
if referent in functionDocumenters:
result.append(functionDocumenters[referent])
- del(functionDocumenters[referent])
+ del functionDocumenters[referent]
for documenter in sorted(
functionDocumenters.values(),
key=lambda x: x.referentPackageSystemPath
@@ -1464,7 +1458,8 @@ def rstPageReferenceFormat(self):
@property
def referenceName(self):
- '''The short name of the module:
+ '''
+ The short name of the module:
>>> from music21 import serial
>>> module = serial
@@ -1490,11 +1485,11 @@ def referenceName(self):
class CorpusDocumenter(Documenter):
- '''A documenter for music21's corpus:
+ '''
+ A documenter for music21's corpus:
>>> documenter = CorpusDocumenter()
>>> restructuredText = documenter.run()
-
'''
# SPECIAL METHODS #
diff --git a/documentation/docbuild/extensions.py b/documentation/docbuild/extensions.py
index 7640d1fe4a..6129036925 100644
--- a/documentation/docbuild/extensions.py
+++ b/documentation/docbuild/extensions.py
@@ -5,9 +5,10 @@
#
# Authors: Josiah Wolf Oberholtzer
#
-# Copyright: Copyright © 2013 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2013-22 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
+from __future__ import annotations
# loaded by source/conf.py
@@ -39,7 +40,9 @@ def fixLines(lines):
lines[:] = newLines
def processDocstring(app, what, name, obj, options, lines):
- '''Process the ``lines`` of each docstring, in place.'''
+ '''
+ Process the ``lines`` of each docstring, in place.
+ '''
# print('WHAT ', what)
# print('NAME ', name)
# print('OBJ ', obj)
diff --git a/documentation/docbuild/iterators.py b/documentation/docbuild/iterators.py
index f897ef7ca7..8ae698ab05 100644
--- a/documentation/docbuild/iterators.py
+++ b/documentation/docbuild/iterators.py
@@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Name: docbuild/iterators.py
-# Purpose: music21 documentation iterators, including IPython notebook to ReST converter
+# Purpose: music21 documentation iterators, including Jupyter notebook to ReST converter
#
# Authors: Josiah Wolf Oberholtzer
-# Michael Scott Cuthbert
+# Michael Scott Asato Cuthbert
#
-# Copyright: Copyright © 2013, 17 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2013, 17 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
+from __future__ import annotations
+
import abc # for @abc.abstractmethod decorator: requires a function to be defined in subclasses
import os
import types
@@ -32,13 +34,13 @@ def __iter__(self):
raise NotImplementedError
-class IPythonNotebookIterator(Iterator):
+class JupyterNotebookIterator(Iterator):
'''
Iterates over music21's documentation directory, yielding .ipynb files.
>>> import os
>>> sp = common.getRootFilePath()
- >>> ipnbi = IPythonNotebookIterator()
+ >>> ipnbi = JupyterNotebookIterator()
>>> for i, nb in enumerate(ipnbi):
... if i >= 3:
... break
@@ -157,7 +159,6 @@ class CodebaseIterator(Iterator):
... print(x)
-
diff --git a/documentation/docbuild/make.py b/documentation/docbuild/make.py
index 8830cd2c8d..fc83ed0a2a 100644
--- a/documentation/docbuild/make.py
+++ b/documentation/docbuild/make.py
@@ -5,11 +5,12 @@
#
# Authors: Josiah Wolf Oberholtzer
# Christopher Ariza
-# Michael Scott Cuthbert
+# Michael Scott Asato Cuthbert
#
-# Copyright: Copyright © 2013-17 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2013-17 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
+from __future__ import annotations
import os
import shutil
@@ -30,7 +31,8 @@ def __init__(self, command='html'):
self.cpus_to_use = common.cpus()
if self.cpus_to_use == 1:
self.useMultiprocessing = False
- self.useMultiprocessing = False # too unstable still
+ else:
+ self.useMultiprocessing = True # now stable enough
self.documentationDirectoryPath = None
self.autogenDirectoryPath = None
self.buildDirectoryPath = None
@@ -41,13 +43,13 @@ def __init__(self, command='html'):
self.getPaths()
- def run(self):
+ def run(self, runSphinx=True):
if self.command == 'clean':
self.runClean()
elif self.command == 'help':
self.print_usage()
else:
- self.runBuild()
+ self.runBuild(runSphinx=runSphinx)
self.postBuild()
def runClean(self):
@@ -76,9 +78,9 @@ def runBuild(self, runSphinx=True):
print('WRITING DOCUMENTATION FILES')
writers.StaticFileCopier().run()
try:
- writers.IPythonNotebookReSTWriter().run()
+ writers.JupyterNotebookReSTWriter().run()
except OSError:
- raise ImportError('IPythonNotebookReSTWriter crashed; most likely cause: '
+ raise ImportError('JupyterNotebookReSTWriter crashed; most likely cause: '
+ 'no pandoc installed: https://github.com/jgm/pandoc/releases')
writers.ModuleReferenceReSTWriter().run()
@@ -89,6 +91,7 @@ def runBuild(self, runSphinx=True):
def runSphinx(self):
try:
+ # noinspection PyPackageRequirements
import sphinx
except ImportError:
message = 'Sphinx is required to build documentation; '
@@ -102,6 +105,8 @@ def runSphinx(self):
# sphinx changed their main processing in v. 1.7; see
# https://github.com/sphinx-doc/sphinx/pull/3668
+ # before 1.7 it ignored the first option thinking it was
+ # always 'sphinx'.
sphinx_version = tuple(sphinx.__version__.split('.'))
sphinx_new = False
if tuple(int(x) for x in sphinx_version[0:2]) < (1, 7):
@@ -126,7 +131,7 @@ def runSphinx(self):
import sphinx.cmd.build # pylint: disable=import-error
sphinx_main_command = sphinx.cmd.build.main
else:
- sphinx_main_command = sphinx.main
+ sphinx_main_command = sphinx.main # pylint: disable=no-member
try: # pylint: disable=assignment-from-no-return
returnCode = sphinx_main_command(sphinxOptions)
diff --git a/documentation/docbuild/nbval-sanitize.cfg b/documentation/docbuild/nbval-sanitize.cfg
index 07c0ae1524..9b3a11ed68 100644
--- a/documentation/docbuild/nbval-sanitize.cfg
+++ b/documentation/docbuild/nbval-sanitize.cfg
@@ -1,3 +1,3 @@
[address]
-regex: 0x[0-9A-Fa-f]+
+regex: \b0x[0-9A-Fa-f]+
replace: ADDRESS
diff --git a/documentation/docbuild/upload.py b/documentation/docbuild/upload.py
index 0ccf4445ba..ae47f0654a 100644
--- a/documentation/docbuild/upload.py
+++ b/documentation/docbuild/upload.py
@@ -5,7 +5,7 @@
#
# Authors: Christopher Ariza
#
-# Copyright: Copyright © 2009-2010, 2013 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2009-2010, 2013 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
# pylint: disable=line-too-long
@@ -49,6 +49,8 @@
Otherwise just contact MSC...
'''
+from __future__ import annotations
+
import getpass
import os
diff --git a/documentation/docbuild/writers.py b/documentation/docbuild/writers.py
index 1a43dd0a00..56210424d3 100644
--- a/documentation/docbuild/writers.py
+++ b/documentation/docbuild/writers.py
@@ -5,11 +5,13 @@
#
# Authors: Josiah Wolf Oberholtzer
# Christopher Ariza
-# Michael Scott Cuthbert
+# Michael Scott Asato Cuthbert
#
-# Copyright: Copyright © 2013-15 Michael Scott Cuthbert and the music21 Project
+# Copyright: Copyright © 2013-22 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
+from __future__ import annotations
+
import logging
import os
import pathlib
@@ -184,7 +186,7 @@ def writeIndexRst(self, referenceNames):
Write the index.rst file from the list of reference names
'''
lines = []
- lines.append('.. moduleReference:')
+ lines.append('.. _moduleReference:')
lines.append('')
lines.append('.. WARNING: DO NOT EDIT THIS FILE:')
lines.append(' AUTOMATICALLY GENERATED.')
@@ -221,27 +223,27 @@ def run(self):
self.write(corpusReferenceFilePath, rst)
-class IPythonNotebookReSTWriter(ReSTWriter):
+class JupyterNotebookReSTWriter(ReSTWriter):
'''
- Converts IPython notebooks into ReST, and handles their associated image
+ Converts Jupyter notebooks into ReST, and handles their associated image
files.
This class wraps the 3rd-party ``nbconvert`` Python script.
'''
def __init__(self):
- from .iterators import IPythonNotebookIterator
+ from .iterators import JupyterNotebookIterator
super().__init__()
- self.ipythonNotebookFilePaths = list(IPythonNotebookIterator())
+ self.jupyterNotebookFilePaths = list(JupyterNotebookIterator())
# Do not run self.setupOutputDirectory()
def run(self):
- for ipythonNotebookFilePath in self.ipythonNotebookFilePaths:
- nbConvertReturnCode = self.convertOneNotebook(ipythonNotebookFilePath)
+ for jupyterNotebookFilePath in self.jupyterNotebookFilePaths:
+ nbConvertReturnCode = self.convertOneNotebook(jupyterNotebookFilePath)
if nbConvertReturnCode is True:
- self.cleanupNotebookAssets(ipythonNotebookFilePath)
- print(f'\tWROTE {common.relativepath(ipythonNotebookFilePath)}')
+ self.cleanupNotebookAssets(jupyterNotebookFilePath)
+ print(f'\tWROTE {common.relativepath(jupyterNotebookFilePath)}')
else:
- print(f'\tSKIPPED {common.relativepath(ipythonNotebookFilePath)}')
+ print(f'\tSKIPPED {common.relativepath(jupyterNotebookFilePath)}')
# do not print anything for skipped -checkpoint files
self.writeIndexRst()
@@ -260,15 +262,16 @@ def writeIndexRst(self):
WROTE autogenerated/usersGuide/index.rst
'''
tocFile = 'usersGuide_99_Table_of_Contents'
- ipFilePaths = [x for x in self.ipythonNotebookFilePaths if 'usersGuide' in x.name]
+ ipFilePaths = [x for x in self.jupyterNotebookFilePaths
+ if 'usersGuide' in x.name and 'usersGuide' in x.parent.name]
if not ipFilePaths:
raise DocumentationWritersException(
- 'No iPythonNotebook files were converted; '
+ 'No Jupyter Notebook files were converted; '
+ 'you probably have a problem with pandoc or nbconvert not being installed.'
)
usersGuideDir = self.notebookFilePathToRstFilePath(ipFilePaths[0]).parent
tocFp = usersGuideDir / (tocFile + '.rst')
- # '/Users/cuthbert/git/music21base/music21/documentation/autogenerated/usersGuide'
+ # '/Users/cuthbert/git/music21base/documentation/autogenerated/usersGuide'
usersGuideInOrder = [tocFile]
with tocFp.open('r', encoding='utf-8') as tocFileHandler:
@@ -298,13 +301,13 @@ def writeIndexRst(self):
self.write(indexFilePath, rst)
- def cleanupNotebookAssets(self, ipythonNotebookFilePath):
+ def cleanupNotebookAssets(self, jupyterNotebookFilePath):
'''
- Deletes all .text files in the directory of ipythonNotebookFilePath
+ Deletes all .text files in the directory of jupyterNotebookFilePath
(a pathlib.Path).
'''
- notebookFileNameWithoutExtension = ipythonNotebookFilePath.stem
- notebookParentDirectoryPath = ipythonNotebookFilePath.parent
+ notebookFileNameWithoutExtension = jupyterNotebookFilePath.stem
+ notebookParentDirectoryPath = jupyterNotebookFilePath.parent
imageFileDirectoryPath = notebookParentDirectoryPath / notebookFileNameWithoutExtension
imageFileDirectoryPath = self.sourceToAutogenerated(imageFileDirectoryPath)
if not imageFileDirectoryPath.exists():
@@ -322,56 +325,56 @@ def rstEditingWarningFormat(self):
return result
- def notebookFilePathToRstFilePath(self, ipythonNotebookFilePath):
- if not ipythonNotebookFilePath.exists():
+ def notebookFilePathToRstFilePath(self, jupyterNotebookFilePath):
+ if not jupyterNotebookFilePath.exists():
raise DocumentationWritersException(
- f'No iPythonNotebook with filePath {ipythonNotebookFilePath}')
- notebookFileNameWithoutExtension = ipythonNotebookFilePath.stem
- notebookParentDirectoryPath = ipythonNotebookFilePath.parent
+ f'No Jupyter Notebook with filePath {jupyterNotebookFilePath}')
+ notebookFileNameWithoutExtension = jupyterNotebookFilePath.stem
+ notebookParentDirectoryPath = jupyterNotebookFilePath.parent
rstFileName = notebookFileNameWithoutExtension + '.rst'
rstFilePath = self.sourceToAutogenerated(notebookParentDirectoryPath / rstFileName)
return rstFilePath
- def convertOneNotebook(self, ipythonNotebookFilePath):
+ def convertOneNotebook(self, jupyterNotebookFilePath):
'''
converts one .ipynb file to .rst using nbconvert.
returns True if IPythonNotebook was converted.
returns False if IPythonNotebook's converted .rst file is newer than the .ipynb file.
- sends AssertionError if ipythonNotebookFilePath does not exist.
+ sends AssertionError if jupyterNotebookFilePath does not exist.
'''
- rstFilePath = self.notebookFilePathToRstFilePath(ipythonNotebookFilePath)
+ rstFilePath = self.notebookFilePathToRstFilePath(jupyterNotebookFilePath)
if rstFilePath.exists():
# print(rstFilePath + ' exists')
# rst file is newer than .ipynb file, do not convert.
- if rstFilePath.stat().st_mtime > ipythonNotebookFilePath.stat().st_mtime:
+ if rstFilePath.stat().st_mtime > jupyterNotebookFilePath.stat().st_mtime:
return False
- self.runNBConvert(ipythonNotebookFilePath)
+ self.runNBConvert(jupyterNotebookFilePath)
# uses this convoluted way of reading because 'encoding' was an invalid keyword argument
# for the built-in 'open' in old python, and never upgraded.
with rstFilePath.open('r', encoding='utf8') as f:
oldLines = f.read().splitlines()
- lines = self.cleanConvertedNotebook(oldLines, ipythonNotebookFilePath)
+ lines = self.cleanConvertedNotebook(oldLines, jupyterNotebookFilePath)
with rstFilePath.open('w', encoding='utf8') as f:
f.write('\n'.join(lines))
return True
- def cleanConvertedNotebook(self, oldLines, ipythonNotebookFilePath):
+ def cleanConvertedNotebook(self, oldLines, jupyterNotebookFilePath):
'''
Take a notebook directly as parsed and make it look better for HTML
Fixes up the internal references to class, ref, func, meth, attr.
'''
- notebookFileNameWithoutExtension = ipythonNotebookFilePath.stem
+ notebookFileNameWithoutExtension = jupyterNotebookFilePath.stem
# imageFileDirectoryName = self.sourceToAutogenerated(notebookFileNameWithoutExtension)
- ipythonPromptPattern = re.compile(r'^In\[[\d ]+]:')
+ jupyterPromptPattern = re.compile(r'^In\[[\d ]+]:')
mangledInternalReference = re.compile(
r':(class|ref|func|meth|attr):``?(.*?)``?')
newLines = [f'.. _{notebookFileNameWithoutExtension}:',
@@ -382,7 +385,7 @@ def cleanConvertedNotebook(self, oldLines, ipythonNotebookFilePath):
while currentLineNumber < len(oldLines):
currentLine = oldLines[currentLineNumber]
# Remove all IPython prompts and the blank line that follows:
- if ipythonPromptPattern.match(currentLine) is not None:
+ if jupyterPromptPattern.match(currentLine) is not None:
currentLineNumber += 2
continue
# Correct the image path in each ReST image directive:
@@ -438,7 +441,7 @@ def blankLineAfterLiteral(self, oldLines):
lines.append(' :class: ipython-result')
return lines
- def runNBConvert(self, ipythonNotebookFilePath):
+ def runNBConvert(self, jupyterNotebookFilePath):
try:
# noinspection PyPackageRequirements
from nbconvert import nbconvertapp as nb
@@ -447,21 +450,21 @@ def runNBConvert(self, ipythonNotebookFilePath):
raise
outputPath = os.path.splitext(
- str(self.sourceToAutogenerated(ipythonNotebookFilePath))
+ str(self.sourceToAutogenerated(jupyterNotebookFilePath))
)[0]
app = nb.NbConvertApp.instance()
app.initialize(argv=['--to', 'rst', '--output', outputPath,
- str(ipythonNotebookFilePath)])
- app.writer.build_directory = str(ipythonNotebookFilePath.parent)
+ str(jupyterNotebookFilePath)])
+ app.writer.build_directory = str(jupyterNotebookFilePath.parent)
app.writer.log.addFilter(_BuildDirectoryFilter())
app.start()
return True
if __name__ == '__main__':
- i = IPythonNotebookReSTWriter()
- p5 = i.ipythonNotebookFilePaths[5]
+ i = JupyterNotebookReSTWriter()
+ p5 = i.jupyterNotebookFilePaths[5]
i.convertOneNotebook(p5)
import music21
music21.mainTest('moduleRelative')
diff --git a/documentation/docbuild_requirements.txt b/documentation/docbuild_requirements.txt
index f2dbb4ab85..a2ec2689e9 100644
--- a/documentation/docbuild_requirements.txt
+++ b/documentation/docbuild_requirements.txt
@@ -1,5 +1,3 @@
-docutils
-pytest
-nbval
-ipython[notebook]>=6.0.0
-sphinx>=1.6
+# for readthedocs.io (based on requirements_dev.txt)
+nbconvert
+sphinx
diff --git a/documentation/make.py b/documentation/make.py
index 2e1ac84214..1bed185976 100755
--- a/documentation/make.py
+++ b/documentation/make.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
+from __future__ import annotations
import sys
+
from docbuild.make import DocBuilder
if __name__ == '__main__':
diff --git a/documentation/nbvalNotebook.py b/documentation/nbvalNotebook.py
index 1a58800feb..9d3fc6796f 100644
--- a/documentation/nbvalNotebook.py
+++ b/documentation/nbvalNotebook.py
@@ -6,21 +6,24 @@
@author: cuthbert
'''
+from __future__ import annotations
+
+import pathlib # for typing
import sys
import subprocess
+
# noinspection PyPackageRequirements
-import pytest # @UnusedImport # pylint: disable=unused-import,import-error
+import pytest # pylint: disable=unused-import,import-error
# noinspection PyPackageRequirements
-import nbval # @UnusedImport # pylint: disable=unused-import,import-error
+import nbval # pylint: disable=unused-import,import-error
from music21 import environment
from music21 import common
-# pytest --nbval usersGuide_15_key.ipynb --sanitize-with ../../nbval-sanitize.cfg -q
+# pytest --nbval usersGuide_15_key.ipynb --nbval-sanitize-with ../../nbval-sanitize.cfg -q
skip = ['installJupyter.ipynb']
-
-def runAll():
+def getAllFiles() -> list[pathlib.Path]:
sourcePath = common.getRootFilePath() / 'documentation' / 'source'
goodFiles = []
for innerDir in ('about', 'developerReference', 'installing', 'usersGuide'):
@@ -32,10 +35,11 @@ def runAll():
continue
goodFiles.append(f)
+ return goodFiles
-
- for f in goodFiles:
- print("Running: ", str(f))
+def runAll():
+ for f in getAllFiles():
+ print('Running: ', str(f))
try:
retVal = runOne(f)
except KeyboardInterrupt:
@@ -44,6 +48,11 @@ def runAll():
if retVal == 512:
return None
+def findAndRun(filename: str):
+ allFiles = getAllFiles()
+ for f in allFiles:
+ if filename in str(f):
+ runOne(f)
def runOne(nbFile):
us = environment.UserSettings()
@@ -61,7 +70,7 @@ def runOne(nbFile):
['pytest',
'--disable-pytest-warnings',
'--nbval', str(nbFile),
- '--sanitize-with', sanitize_fn,
+ '--nbval-sanitize-with', sanitize_fn,
'-q'],
check=False,
)
diff --git a/documentation/scripts/find-docstrings-with-unindented-code-blocks b/documentation/scripts/find-docstrings-with-unindented-code-blocks
index debdc571ed..065a405734 100755
--- a/documentation/scripts/find-docstrings-with-unindented-code-blocks
+++ b/documentation/scripts/find-docstrings-with-unindented-code-blocks
@@ -1,8 +1,16 @@
#! /usr/bin/env python
+'''
+Not used: we do not indent code blocks.
+'''
import inspect
+import pathlib
+import sys
import types
-from music21.documentation.iterators import CodebaseIterator
+
+sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
+
+from docbuild.iterators import CodebaseIterator
def hasUnindentedCodeBlocks(docstring):
@@ -14,15 +22,14 @@ def hasUnindentedCodeBlocks(docstring):
def main():
- print 'Inspecting docstrings...'
- print
+ print('Inspecting docstrings...\n')
count = 0
for x in CodebaseIterator():
address = '.'.join((x.__module__, x.__name__))
# inspect.getdoc() strips whitespace for us
docstring = inspect.getdoc(x)
if docstring is not None and hasUnindentedCodeBlocks(docstring):
- print address
+ print(address)
count += 1
if isinstance(x, type):
for attr in inspect.classify_class_attrs(x):
@@ -31,13 +38,12 @@ def main():
attrAddress = ':'.join((address, attr.name))
docstring = inspect.getdoc(attr.object)
if docstring is not None and hasUnindentedCodeBlocks(docstring):
- print attrAddress
+ print(attrAddress)
count += 1
if count:
- print
- print 'Found {} docstring(s) with unindented code blocks.'.format(count)
+ print(f'Found {count} docstring(s) with unindented code blocks.')
else:
- print 'Found no docstrings with unindented code blocks.'
+ print('Found no docstrings with unindented code blocks.')
if __name__ == '__main__':
diff --git a/documentation/scripts/find-docstrings-without-leading-newline b/documentation/scripts/find-docstrings-without-leading-newline
index e599ebe8b9..a112f13061 100755
--- a/documentation/scripts/find-docstrings-without-leading-newline
+++ b/documentation/scripts/find-docstrings-without-leading-newline
@@ -1,34 +1,42 @@
#! /usr/bin/env python
+import enum
import inspect
+import pathlib
+import sys
import types
-from music21.documentation.iterators import CodebaseIterator
+sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
+
+from docbuild.iterators import CodebaseIterator
def main():
- print 'Inspecting docstrings...'
- print
+ print('Inspecting docstrings...\n')
count = 0
- for x in CodebaseIterator():
+ for x in CodebaseIterator(verbose=False):
+ if isinstance(x, type) and issubclass(x, (tuple, enum.Enum)): # named tuple
+ continue
address = '.'.join((x.__module__, x.__name__))
docstring = x.__doc__
- if docstring is not None and not docstring.startswith('\n'):
- print address
+ if docstring and not docstring.startswith('\n'):
+ print(address)
count += 1
if isinstance(x, type):
for attr in inspect.classify_class_attrs(x):
if attr.defining_class is not x or attr.name.startswith('_'):
continue
+ if attr.kind == 'data':
+ continue
attrAddress = ':'.join((address, attr.name))
docstring = attr.object.__doc__
- if docstring is not None and not docstring.startswith('\n'):
- print attrAddress
+ if docstring and not docstring.startswith('\n'):
+ print(attrAddress)
count += 1
if count:
- print
- print 'Found {} docstring(s) without leading newlines.'.format(count)
+ print()
+ print(f'Found {count} docstring(s) without leading newlines.')
else:
- print 'Found no docstrings without leading newlines.'
+ print('Found no docstrings without leading newlines.')
if __name__ == '__main__':
diff --git a/documentation/source/_themes/m21/layout.html b/documentation/source/_themes/m21/layout.html
index a0575f343c..190459323d 100644
--- a/documentation/source/_themes/m21/layout.html
+++ b/documentation/source/_themes/m21/layout.html
@@ -5,8 +5,12 @@
{%- endblock -%}
{%- block extrahead -%}
-
-
+
+
+
+
+
+
{%- endblock -%}
{%- block scripts %}
diff --git a/documentation/source/_themes/m21/static/m21.css b/documentation/source/_themes/m21/static/m21.css
index eeae1015e4..b04f0ddd0f 100644
--- a/documentation/source/_themes/m21/static/m21.css
+++ b/documentation/source/_themes/m21/static/m21.css
@@ -7,9 +7,9 @@
/* -- page layout ----------------------------------------------------------- */
body {
- font-family: 'Open Sans', sans-serif;
+ font-family: 'Bitter', serif;
font-size: 15px;
- line-height: 150%;
+ line-height: 160%;
text-align: center;
color: #2b3600;
padding: 0;
@@ -18,7 +18,7 @@ body {
-moz-box-shadow: 0px 0px 10px #93a1a1;
-webkit-box-shadow: 0px 0px 10px #93a1a1;
box-shadow: 0px 0px 10px #93a1a1;
- background: #b3cccc;
+ background: #93787e;
}
.clearer {
@@ -27,6 +27,11 @@ body {
padding: 0 0 0 0;
}
+.strike {
+ text-decoration: line-through;
+}
+
+
pre, div[class*="highlight-"] {
clear: left;
}
@@ -55,7 +60,7 @@ div.body {
div.related {
font-size: 1em;
/*background: #002b36;*/
- background-color: #2e3436;
+ background-color: #473d42;
color: #839496;
padding: 5px 0px;
}
@@ -118,11 +123,13 @@ div.sphinxsidebar h3, div.sphinxsidebar h4 {
margin: 1em 0 0.5em 0;
font-size: 1em;
padding: 0.7em;
- background-color: #eeeff1;
+ color: white;
+ background-color: #7a4f6d;
+ border-radius: 8px;
}
div.sphinxsidebar h3 a {
- color: #2E3436;
+ color: #f9c7d1;
}
div.sphinxsidebar ul {
@@ -162,17 +169,18 @@ p {
}
div.body a, div.sphinxsidebarwrapper a {
- color: #dc322f;
+ color: #704c97;
/* color: #268bd2; */
text-decoration: none;
}
div.body a:hover, div.sphinxsidebarwrapper a:hover {
- border-bottom: 1px solid #268bd2;
+ border-bottom: 1px solid #d22688;
+ // this is the text-decoration alternative
}
h1, h2, h3, h4, h5, h6, p.rubric {
- font-family: "Open Sans", sans-serif;
+ font-family: 'Bitter', serif;
font-weight: 400;
}
@@ -266,9 +274,9 @@ a.headerlink:hover {
cite, code, tt {
- font-family: 'Source Code Pro', monospace;
- font-size: 0.9em;
- letter-spacing: 0.05em;
+ font-family: 'Courier Prime', monospace;
+ font-size: 1.1em;
+ // letter-spacing: 0.05em;
font-style: normal;
}
@@ -279,19 +287,19 @@ hr {
}
.highlight {
- -webkit-border-radius: 2px;
- -moz-border-radius: 2px;
- border-radius: 2px;
- background-color: #eef6f6;
+ border-radius: 8px;
+ background-color: #ededed;
}
div.ipython-result div.highlight {
- background-color: #e8e8db;
- margin-top: -12px; /* close the gap! */
+ background-color: #e8dbdb;
+ border-top-left-radius: 0px;
+ border-top-right-radius: 0px;
+ margin-top: -20px; /* close the gap! */
}
pre {
- font-family: 'Source Code Pro', monospace;
+ font-family: 'Courier Prime', monospace;
font-style: normal;
font-size: 0.9em;
letter-spacing: 0.015em;
@@ -420,16 +428,12 @@ div.viewcode-block:target {
background-color: #FFFFf0;
/*border: 1px dashed #aaa;*/
color: #304030;
- font-family: 'Source Code Pro', monospace;
+ font-family: 'Courier Prime', monospace;
font-style: normal;
font-size: 0.9em;
letter-spacing: 0.015em;
line-height: 120%;
padding: 0.7em;
- white-space: pre-wrap;
- white-space: -moz-pre-wrap;
- white-space: -pre-wrap;
- white-space: -o-pre-wrap;
word-wrap: break-word;
}
@@ -443,3 +447,47 @@ div.admonition::after,
blockquote::after {
clear: none;
}
+
+/* function and class parameters on their own lines */
+/* essential for typed params. See https://github.com/sphinx-doc/sphinx/issues/1514 */
+/* Newlines (\a) and spaces (\20) before each parameter */
+.sig-param::before {
+ content: "\a\20\20\20\20";
+ white-space: pre;
+}
+
+/* Newline after the last parameter (so the closing bracket is on a new line) */
+dt em.sig-param:last-of-type::after {
+ content: "\a";
+ white-space: pre;
+}
+
+/* parameter name in method/function and ':' */
+dt em.sig-param .n, dt em.sig-param .p {
+ font-style: normal;
+}
+
+/* equal sign small space. */
+dt em.sig-param .o {
+ font-style: normal;
+ padding-left: 2px;
+ padding-right: 2px;
+}
+
+/* ...except if there already is a space. */
+dt em.sig-param .w + .o {
+ padding-left: 0px;
+ padding-right: 0px;
+}
+
+
+/* When linked from somewhere else */
+dt:target, span.highlighted {
+ /* yellow was too obnoxious */
+ background-color: #ffeedb;
+}
+
+/* To have blue background of width of the block (instead of width of content) */
+dl.class > dt:first-of-type {
+ display: block !important;
+}
diff --git a/documentation/source/_themes/m21/static/pygments.css b/documentation/source/_themes/m21/static/pygments.css
index 6e56f022e3..47963ea802 100644
--- a/documentation/source/_themes/m21/static/pygments.css
+++ b/documentation/source/_themes/m21/static/pygments.css
@@ -1,71 +1,78 @@
/* Pygments not copying the proper file, so this is in the m21 theme */
+/* Currently, this is Lovelace */
-// .highlight .hll { background-color: #ffffcc }
-// .highlight { background: #ffffff; }
-.highlight .c { color: #888888 } /* Comment */
-.highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */
-.highlight .k { color: #008800; font-weight: bold } /* Keyword */
-.highlight .o { color: #336633 } /* Operator */
-.highlight .ch { color: #888888 } /* Comment.Hashbang */
-.highlight .cm { color: #888888 } /* Comment.Multiline */
-.highlight .cp { color: #557799 } /* Comment.Preproc */
-.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
-.highlight .c1 { color: #888888 } /* Comment.Single */
-.highlight .cs { color: #cc0000; font-weight: bold } /* Comment.Special */
-.highlight .gd { color: #A00000 } /* Generic.Deleted */
+pre { line-height: 125%; }
+td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+.highlight .hll { background-color: #ffffcc }
+.highlight { background: #ffffff; }
+.highlight .c { color: #888888; font-style: italic } /* Comment */
+.highlight .err { background-color: #a848a8 } /* Error */
+.highlight .k { color: #2838b0 } /* Keyword */
+.highlight .o { color: #666666 } /* Operator */
+.highlight .p { color: #888888 } /* Punctuation */
+.highlight .ch { color: #287088; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #289870 } /* Comment.Preproc */
+.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #888888; font-style: italic } /* Comment.Special */
+.highlight .gd { color: #c02828 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
-.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
-.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
+.highlight .gr { color: #c02828 } /* Generic.Error */
+.highlight .gh { color: #666666 } /* Generic.Heading */
+.highlight .gi { color: #388038 } /* Generic.Inserted */
+.highlight .go { color: #666666 } /* Generic.Output */
+.highlight .gp { color: #444444 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
-.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.highlight .gt { color: #0044DD } /* Generic.Traceback */
-.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
-.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
-.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
-.highlight .kp { color: #003388; font-weight: bold } /* Keyword.Pseudo */
-.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
-.highlight .kt { color: #333399; font-weight: bold } /* Keyword.Type */
-.highlight .m { color: #6600EE; font-weight: bold } /* Literal.Number */
-.highlight .s { font-style: italic; color: #a0a0ff } /* Literal.String */
-.highlight .na { color: #0000CC } /* Name.Attribute */
-.highlight .nb { color: #007020 } /* Name.Builtin */
-.highlight .nc { color: #BB0066; font-weight: bold } /* Name.Class */
-.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
-.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */
-.highlight .ni { color: #880000; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #FF0000; font-weight: bold } /* Name.Exception */
-.highlight .nf { color: #0066BB; font-weight: bold } /* Name.Function */
-.highlight .nl { color: #997700; font-weight: bold } /* Name.Label */
-.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
-.highlight .nt { color: #007700 } /* Name.Tag */
-.highlight .nv { color: #996633 } /* Name.Variable */
-.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */
-.highlight .w { color: #bbbbbb } /* Text.Whitespace */
-.highlight .mb { color: #6600EE; font-weight: bold } /* Literal.Number.Bin */
-.highlight .mf { color: #6600EE; font-weight: bold } /* Literal.Number.Float */
-.highlight .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */
-.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
-.highlight .mo { color: #4400EE; font-weight: bold } /* Literal.Number.Oct */
-.highlight .sa { background-color: #fff0f0 } /* Literal.String.Affix */
-.highlight .sb { background-color: #fff0f0 } /* Literal.String.Backtick */
-.highlight .sc { color: #0044DD } /* Literal.String.Char */
-.highlight .dl { background-color: #fff0f0 } /* Literal.String.Delimiter */
-.highlight .sd { color: #DD4422 } /* Literal.String.Doc */
-.highlight .s2 { font-style: italic; color: #a0a0ff } /* Literal.String.Double */
-.highlight .se { color: #666666; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */
-.highlight .sh { font-style: italic; color: #a0a0ff } /* Literal.String.Heredoc */
-.highlight .si { background-color: #eeeeee } /* Literal.String.Interpol */
-.highlight .sx { color: #DD2200; background-color: #fff0f0 } /* Literal.String.Other */
-.highlight .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */
-.highlight .s1 { font-style: italic; color: #a0a0ff } /* Literal.String.Single */
-.highlight .ss { color: #AA6600 } /* Literal.String.Symbol */
-.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
-.highlight .fm { color: #0066BB; font-weight: bold } /* Name.Function.Magic */
-.highlight .vc { color: #336699 } /* Name.Variable.Class */
-.highlight .vg { color: #dd7700; font-weight: bold } /* Name.Variable.Global */
-.highlight .vi { color: #3333BB } /* Name.Variable.Instance */
-.highlight .vm { color: #996633 } /* Name.Variable.Magic */
-.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
\ No newline at end of file
+.highlight .gu { color: #444444 } /* Generic.Subheading */
+.highlight .gt { color: #2838b0 } /* Generic.Traceback */
+.highlight .kc { color: #444444; font-style: italic } /* Keyword.Constant */
+.highlight .kd { color: #2838b0; font-style: italic } /* Keyword.Declaration */
+.highlight .kn { color: #2838b0 } /* Keyword.Namespace */
+.highlight .kp { color: #2838b0 } /* Keyword.Pseudo */
+.highlight .kr { color: #2838b0 } /* Keyword.Reserved */
+.highlight .kt { color: #2838b0; font-style: italic } /* Keyword.Type */
+.highlight .m { color: #444444 } /* Literal.Number */
+.highlight .s { color: #b83838 } /* Literal.String */
+.highlight .na { color: #388038 } /* Name.Attribute */
+.highlight .nb { color: #388038 } /* Name.Builtin */
+.highlight .nc { color: #287088 } /* Name.Class */
+.highlight .no { color: #b85820 } /* Name.Constant */
+.highlight .nd { color: #287088 } /* Name.Decorator */
+.highlight .ni { color: #709030 } /* Name.Entity */
+.highlight .ne { color: #908828 } /* Name.Exception */
+.highlight .nf { color: #785840 } /* Name.Function */
+.highlight .nl { color: #289870 } /* Name.Label */
+.highlight .nn { color: #289870 } /* Name.Namespace */
+.highlight .nt { color: #2838b0 } /* Name.Tag */
+.highlight .nv { color: #b04040 } /* Name.Variable */
+.highlight .ow { color: #a848a8 } /* Operator.Word */
+.highlight .w { color: #a89028 } /* Text.Whitespace */
+.highlight .mb { color: #444444 } /* Literal.Number.Bin */
+.highlight .mf { color: #444444 } /* Literal.Number.Float */
+.highlight .mh { color: #444444 } /* Literal.Number.Hex */
+.highlight .mi { color: #444444 } /* Literal.Number.Integer */
+.highlight .mo { color: #444444 } /* Literal.Number.Oct */
+.highlight .sa { color: #444444 } /* Literal.String.Affix */
+.highlight .sb { color: #b83838 } /* Literal.String.Backtick */
+.highlight .sc { color: #a848a8 } /* Literal.String.Char */
+.highlight .dl { color: #b85820 } /* Literal.String.Delimiter */
+.highlight .sd { color: #b85820; font-style: italic } /* Literal.String.Doc */
+.highlight .s2 { color: #b83838 } /* Literal.String.Double */
+.highlight .se { color: #709030 } /* Literal.String.Escape */
+.highlight .sh { color: #b83838 } /* Literal.String.Heredoc */
+.highlight .si { color: #b83838; text-decoration: underline } /* Literal.String.Interpol */
+.highlight .sx { color: #a848a8 } /* Literal.String.Other */
+.highlight .sr { color: #a848a8 } /* Literal.String.Regex */
+.highlight .s1 { color: #b83838 } /* Literal.String.Single */
+.highlight .ss { color: #b83838 } /* Literal.String.Symbol */
+.highlight .bp { color: #388038; font-style: italic } /* Name.Builtin.Pseudo */
+.highlight .fm { color: #b85820 } /* Name.Function.Magic */
+.highlight .vc { color: #b04040 } /* Name.Variable.Class */
+.highlight .vg { color: #908828 } /* Name.Variable.Global */
+.highlight .vi { color: #b04040 } /* Name.Variable.Instance */
+.highlight .vm { color: #b85820 } /* Name.Variable.Magic */
+.highlight .il { color: #444444 } /* Literal.Number.Integer.Long */
diff --git a/documentation/source/_themes/m21/theme.conf b/documentation/source/_themes/m21/theme.conf
index 21e59d7cbc..ade5e5e584 100644
--- a/documentation/source/_themes/m21/theme.conf
+++ b/documentation/source/_themes/m21/theme.conf
@@ -1,5 +1,6 @@
[theme]
inherit = basic
stylesheet = m21.css
-pygments_style = fruity
-# note that pygments_style is not currently used
+pygments_style = 'lovelace'
+# note that pygments_style is not currently used for some reason, so copy
+# the appropriate CSS file from pygments to documentation/source/_themes/m21/static/pygments.css
diff --git a/documentation/source/about/about.rst b/documentation/source/about/about.rst
index 22c33dcc8f..29fe309bc6 100644
--- a/documentation/source/about/about.rst
+++ b/documentation/source/about/about.rst
@@ -48,7 +48,7 @@ Acknowledgements
Funding
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The music21 project was made possible by generous research funding
+`music21` was made possible by generous research funding
from the **Seaver Institute** and
the **National Endowment for the Humanities**/Digging into Data research fund.
@@ -76,7 +76,7 @@ working on other music and technology projects, in particular:
contributing to the knowledge of music since 1984, and
publishers of *Computing in Musicology*.
-.. _David Huron: https://www.musiccognition.osu.edu/people/david-huron/
+.. _David Huron: https://music.osu.edu/people/david-huron
.. _Humdrum: https://www.humdrum.org
.. _Michael Good: https://www.musicxml.com
.. _Center for Computer-Assisted Research in the Humanities: http://www.ccarh.org/
@@ -137,7 +137,7 @@ to this project. Their contributions and generosity are greatly appreciated.
* `Project Gutenberg`_ houses public domain music, including the quartets of Beethoven,
Haydn, and Mozart, in musicxml format which we have been able to include in music21.
-.. _Donald Byrd: https://homes.luddy.indiana.edu/donbyrd/CMNExtremes.htm
+.. _Donald Byrd: https://web.archive.org/web/20220610200930/https://homes.luddy.indiana.edu/donbyrd/CMNExtremes.htm
.. _Laura E. Conrad: http://www.serpentpublications.org/drupal7/
.. _MuseScore: https://musescore.com/
.. _Bryen Travis: http://www.bachcentral.com/
@@ -174,7 +174,7 @@ Licensing and Copyright
The `music21` Toolkit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Music21 is Copyright © 2006-2021, Michael Scott Cuthbert and cuthbertLab.
+Music21 is Copyright © 2006-2023 Michael Scott Asato Cuthbert.
Music21 code (excluding content encoded in the corpus) is
free and open-source software, licensed under the BSD License.
diff --git a/documentation/source/about/faq.rst b/documentation/source/about/faq.rst
index 94137f2096..d948baab46 100644
--- a/documentation/source/about/faq.rst
+++ b/documentation/source/about/faq.rst
@@ -32,13 +32,17 @@ Why is it called `music21`?
I figured that since no one had actually released a piece of software
in over 35 years that actually followed the "Music N" naming structure,
- it was fair game to think of it not as the paradigmatic name for a
+ it was fair game to think of it not as the paradigmatic name just for a
software synthesis package, but for any new or original usage of software
and music.
- I'm pretty tickled that the new naming convention is getting an hommage
+ I'm pretty tickled that the new naming convention is getting an homage
in Anas Ghrab's music22 package: https://pypi.org/project/music22
-
+
+ When I was giving a talk in Germany, everyone referred to it as
+ Musikeinundzwanzig, and that made me happy, so feel free to localize
+ the name however you like when speaking in your language.
+ Musique vignt-et-un, 音楽二十一, whatever.
What is the native `music21` data format?
@@ -70,7 +74,8 @@ What is the native `music21` data format?
(This is the reverse of the problem Finale has,
where you can only read older versions of files), so it
should not be used for long term storage and sharing.
- Run :meth:`~music21.stream.Stream.setupSerializationScaffold` and
+ Run `s.freeze()` and then `s.thaw()` later. Or for more deeply configurable
+ needs, :meth:`~music21.stream.Stream.setupSerializationScaffold` and
:meth:`~music21.stream.Stream.teardownSerializationScaffold`
before running `pickle.dump(yourStream)` and
after running `pickle.load(yourStream)`
@@ -79,7 +84,7 @@ What is the native `music21` data format?
* For data that cannot easily be recreated but needs long-term storage and sharing,
try saving the relevant
parts of the data in an XML, json, or even CSV(!) format.
- See plistlib https://docs.python.org/dev/library/plistlib.html
+ See plistlib at https://docs.python.org/dev/library/plistlib.html
or https://code.activestate.com:443/recipes/440595. You cannot store every
element of python's object structure,
but you should easily be able to store the parts that are difficult to recreate.
@@ -91,14 +96,28 @@ What is the native `music21` data format?
Please consider contributing
your solution to improve the package.
+I'm ready to give it a try, but how do I install `music21`?
+
+ Please see the complete install instructions:
+ :ref:`User's Guide, Chapter 1: Installing music21 `.
+
+I'm using Linux/GNU/Unix and I'm having trouble configuring `music21`, can
+you help?
+
+ Unfortunately, due to the number of different flavors of open-source
+ operating systems, the development team can only give free help to
+ Mac and Windows installation. (but see paid support, below).
+
+Uses
+---------
Can I synthesize new sounds with `music21`?
- Yes, and no. `Music21` is primarily built for manipulating symbolic
+ Yes, and no. `Music21` is primarily built for manipulating symbolic
musical data not sound waves. There are lots of great programs for
- doing that. But the symbolic data, however, can be used as data within
+ doing that. But the symbolic data, however, can be used as data within
a large variety of synthesis packages. So you could create new
- `music21` objects that control the synthesis package of your choosing.
-
+ `music21` objects that control the synthesis package of your choosing.
+
Can `music21` read in music directly from an image or a .pdf?
Sorry, that's beyond `music21`'s capabilities. This technology
@@ -108,19 +127,27 @@ Can `music21` read in music directly from an image or a .pdf?
with the commercial SmartScore application. `Music21` does have a
good set of modules for improving the output of OMR after it's done.
See :ref:`moduleOmrCorrectors`.
-
-I'm ready to give it a try, but how do I install `music21`?
- Please see the complete install instructions:
- :ref:`User's Guide, Chapter 1: Installing music21 `.
+I'm having trouble reading in MIDI. The docs for `music21.midi.translate`
+and `music21.converter.subConverters.ConverterMIDI` are rather hard
+to understand. (Same for `musicxml`, `mei`, etc.)
-I'm using Linux/GNU/Unix and I'm having trouble configuring `music21`, can
-you help?
+ You shouldn't need to be going into subConverters or the various format
+ modules, like `music21.midi` or `music21.musicxml` in everyday use.
+ Just load the file into the system with `music21.converter.parse(filename)`
+ and it will figure out the right format and everything else, and give you
+ a `Stream` object. To load
+ from the internet, you can pass in a URL instead. If
+ `filename` does not give enough information to determine the file type,
+ add `format='midi'` and it will do the right thing.
+
+ To save a `Stream` (stored as variable `s`) afterwards, just call:
+ `s.write('midi', fp='filename.mid')`. Or to hear it immediately,
+ `s.show('midi')`.
- Unfortunately, due to the number of different flavors of open-source
- operating systems, the development team can only give free help to
- Mac and Windows installation. (but see paid support, below).
+Feature requests and Consulting
+-------------------------------
`Music21` doesn't have some feature that I need, how do I get it added?
It won't hurt to email the list (or us directly) and we'll consider it.
@@ -132,12 +159,10 @@ you help?
employ. (Or side-step us and offer a cash bounty on the music21list
itself).
-Consulting
-----------
No, you don't understand, I **really** need this feature!
If you really need something done in `music21`, we offer paid support
- by the hour at standard consulting rates. Contact `cuthbert@mit.edu`
+ by the hour at standard consulting rates. Contact `michael.asato.cuthbert@gmail.com`
for details and rates.
Is this also what I should do if I need help using `music21` for my own project?
diff --git a/documentation/source/about/what.ipynb b/documentation/source/about/what.ipynb
index f92a7850f4..1315bc8b83 100644
--- a/documentation/source/about/what.ipynb
+++ b/documentation/source/about/what.ipynb
@@ -1,25 +1,23 @@
{
"cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "# ignore this\n",
- "%load_ext music21.ipython21"
- ]
- },
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"# What is `music21`?"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"`Music21` is a Python-based toolkit for computer-aided musicology.\n",
"\n",
@@ -34,14 +32,22 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"## Finding solutions in a hurry"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"With `music21` adds a collection of specialized tools and objects to the general-purpose and easy to understand \"Python\" programming language. Install `music21` and type `python3` (or, better, `ipython`) and load it by typing:"
]
@@ -49,7 +55,11 @@
{
"cell_type": "code",
"execution_count": 2,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"from music21 import *"
@@ -57,7 +67,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"...and thousands of musical tools become available to you. For instance, want to see a note on the screen? Type these lines:"
]
@@ -65,11 +79,15 @@
{
"cell_type": "code",
"execution_count": 3,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"data": {
- "image/png": "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\n",
+ "image/png": "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\n",
"text/plain": [
""
]
@@ -77,7 +95,7 @@
"metadata": {
"image/png": {
"height": 36,
- "width": 199
+ "width": 211
}
},
"output_type": "display_data"
@@ -91,7 +109,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Need a whole line of notes? Even easier:"
]
@@ -99,11 +121,15 @@
{
"cell_type": "code",
"execution_count": 4,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"data": {
- "image/png": "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\n",
+ "image/png": "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\n",
"text/plain": [
""
]
@@ -124,15 +150,22 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Want to hear the melody? It's just as easy! (Please give it a second or two after hitting play for the piano sounds to load):"
]
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 6,
"metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ },
"tags": [
"nbval-ignore-output"
]
@@ -142,19 +175,32 @@
"data": {
"text/html": [
"\n",
- " \n",
- " \n",
- " "
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " "
],
"text/plain": [
""
@@ -170,15 +216,23 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Want to view the opening tone-row of Schoenberg's Fourth String quartet as a matrix?"
]
},
{
"cell_type": "code",
- "execution_count": 6,
- "metadata": {},
+ "execution_count": 7,
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -205,36 +259,27 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Get a quick graph showing how common various pitches are in a fourteenth century piece:"
]
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 3,
"metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ },
"tags": [
"nbval-ignore-output"
]
},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAABI8AAAL9CAYAAACiz0byAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAABYlAAAWJQFJUiTwAABPMElEQVR4nO3de5iVZb0//jdymAE8gBAM4gE2FIaHVDDzCJ43aVCaeQZJSqtvlrV3mVsTNXdWWlS7bSYFppiV2fZYiAoiO03BKyRDLUHzAAqCmCDIYf3+8Mdsp5kHWTDMYsHrdV1czdzP6TOftRye3tzPvVqVSqVSAAAAAKAJ21S6AAAAAAA2X8IjAAAAAAoJjwAAAAAoJDwCAAAAoJDwCAAAAIBCwiMAAAAACgmPAAAAACgkPAIAAACgkPAIAAAAgELCIwAAAAAKCY8AAAAAKCQ8AgAAAKCQ8AgAAACAQsIjANiKnXXWWWnVqtU6/+ywww456KCD8r3vfS/Lly9vdI5XXnkl/fr1y4EHHtjk9k2lb9++DeocP358s517ypQphf0466yzGu3fq1evwv3fafny5TnwwAPTr1+/vPLKK81WLwDApiQ8AoCt2BVXXJFZs2blG9/4Rv3YrFmz6v9MnTo1l1xySZ566ql86Utfygc/+MEsWLCgwTmmTp2ap59+Og8//HCeeOKJRtcYP358WrVqlcGDBzdr7b/73e8ya9asDBw4sFnPmyT7779/fQ/W+sY3vpFZs2bliiuuaLT/Pffck1mzZmXYsGFJkoEDBzY6Pkn+/Oc/5+GHH87TTz+dBx98sFlqXRtcTZkypVnOBwDwz9pUugAAoHJ69uyZnj17Zvr06fVje+65Z4N9Dj300AwdOjR77rlnZs2albPOOit33XVX/fZjjz02H/nIR9KpU6fss88+LVV63vve9yZJOnbs2Ozn7tixY6M+9OzZs9HYWu973/uSJJ06dSo8Pkn23XffDB8+PIsWLcqxxx7bvEUDAGwiwiMA4F317ds3p556asaPH5+77747c+bMyb/8y78kSbbbbrvcfvvtFa6wOrRu3TrXX399pcsAACiLx9YAgPWyxx571H/9pz/9qXKFAADQooRHAMB6adeuXf3Xq1atStJ4oeh3Llq9dq2jkSNHJkkeeOCBBvs2tQbS66+/nssuuyz77rtvtttuu9TW1qZ379458cQTc/3112fZsmXrrPHJJ5/Mxz/+8XTt2jXt27fPfvvtl1/96lcb/8M3g8GDBzf4+UePHt1onzfeeCNXX311Bg4cmK5du6ampiZ9+vTJqaeemgkTJjT4+df2/rnnnkuSHH744Q3O39QaSFOmTMkJJ5yQHj16pF27dqmrq8vHPvax3H///eusfdasWTnxxBPTtWvX1NbWpk+fPvnCF76QRYsWNfq5xowZkyRp06ZNo3oee+yxnHDCCenWrVu22WabBguQr1mzJr/73e/yqU99Kv3790+HDh1SW1ubvn375jOf+Uzmzp3bqK4XXnih0QLlzz77bO6444586EMfSseOHbPzzjvns5/9bF599dUkyZtvvpmvfe1r2XXXXVNbW5u99947v/jFL9bjFQSArZfwCABYL3/961/rv959992T/N9C0U0tWv3Rj360wWLc71xEetasWRk3blyD/Z9++ul84AMfyGWXXZbBgwfnd7/7Xe69996cd955efDBB3PWWWfVL0jdlNmzZ+fUU0/Nxz/+8dx999353ve+l7/97W85+eST89vf/rY5WrBRxo0b12BR7X/2xhtv5IADDsi///u/Z/Dgwbnlllvy4IMP5sILL8wjjzySM844I9/5znfq91/b+5122ilJ8rOf/axBf/fff/8G5//a176Www8/PH/7298yZsyYPPTQQ/nhD3+YOXPm5Mgjj8xXvvKVJuu65557sv/+++eOO+7IZz7zmTzwwAP52c9+lmXLluXAAw/Ma6+9luT/FhQ/88wzkyQzZ85sUN+UKVNyxhln5MQTT8zdd9+d73//+9lmm/+7Ff3LX/6SD3/4w/nVr36V4cOHZ9KkSfn973+fkSNH5qabbsq+++6bGTNmNKitrq6u0cLkN954Y8aOHZtvfvObueuuu3LooYfmmmuuyZAhQ7Jy5cqcddZZ2XXXXfOb3/wmP/rRj/L888/ntNNOy2233bY+LyMAbJ1KAMBWb9y4caUkpaJbg0WLFpU6d+5cSlLab7/9Gm0fNGhQKUlp3LhxheceNGhQ4fWXLl1aet/73ldKUrrqqqsabX/sscdKbdu2bfIca6+97bbblubOndtg29ixY0tJSgcccEDhtd/N2r409bP9sxEjRrzrz7p2n0suuaTB+Pe+971SktIZZ5zR6Ji5c+eWamtrGx1TKpVKu+22WylJafLkyYXXvO6660pJSrvuumtp8eLFDbYtXry4tOuuu5aSlK699toG2xYsWFDq0qVLKUnpmmuuaXTe//f//t+79mdtfTvuuGPphRdeaLDt6KOPLo0YMaJUKpVKs2bNKiUp/fa3v210jsmTJ5datWpV2muvvUpr1qxp8jpr6zjggANKq1atqh9fs2ZNqV+/fqUkpRNPPLHR+X/+85+XkpQOPPDAJs8LAJRKZh4BAIVef/31TJ48OYcffngWL16crl275oYbbmj26/z0pz/N008/nS5duuS8885rtH3ffffN4Ycfvs5zHHfccenVq1eDsbXHzJgxI2+99dZG1Xj22WenTZs26/zz85//fIPP/+STTyZp+tPjevXqlU996lP1n+pWjrfeeisXX3xxkuQLX/hC/SfCrdWpU6d88YtfTJJcfPHFWbFiRf22a6+9Nq+++mq6deuWT33qU43OfeGFFzaYPbQuZ5xxRnr27Nlg7KabbspVV12VJOnWrVsuu+yyDB06tNGxgwcPTu/evTNr1qw88cQT67zO2WefndatW9d//85HJB9++OF89KMfbbD/kUcemSR59NFHs3LlyvX6WQBga+PT1gCABtq0+b/bg9WrVydJdthhh3zyk5/MZZdd1igAaA633HJLkuTQQw9N27Ztm9znhz/8Yf0jUk0ZMGBAo7G1j0ytWrUqr776anr06LHBNV522WXrfGwuSS666KINfvypb9++Sd5eK+qDH/xgzjzzzAa9+MEPfrBB5/3DH/6Q+fPnJ0mOOOKIJvc56qijkiSvvPJKpk2bVh+o3HnnnUmSQw45pEEgs1aPHj3Su3fvPPPMM+9ax0EHHdRorGvXrvVfd+vWrT7kasouu+ySOXPm5Kmnnsqee+5ZuN++++7baOw973lPkmSfffZptK1bt25J3n6PLFy4cKPeIwCwpRIeAQANvPOT1Fq1apXtt98+PXv2XO8ZJhviz3/+c5I0mjn0Tu8266ZLly6Nxmpra+u/fueMmg3Rs2fPdYYWSRrN6inHueeemxtvvDEzZ87M2Wefna985SsZOnRohg0bliFDhjRYsLwc71wPqHfv3k3u886+z5o1qz48Wjsbatdddy08f/fu3dcrPHpnUFTk+eefz3e/+93cd999ee6557Js2bKUSqUk/xdkvvHGG+s8x4477thobG3w1dS2d4alG/seAYAtlcfWAIAG9txzz/o/e+yxR3bZZZdNGhwlbz8elyTt27ff4HNs6ho3tW233TYPP/xwvvvd76Zfv3559dVXM27cuHz0ox/NLrvskquuuqo+SCnH2t4mxf3t0KFD/ddLliyp//of//jHOo9LUjhT7J81NXPpnR566KHsscceGTNmTPbdd9/84he/yPTp0/OnP/0pf/rTn+oXZX+3HqzrfVDt7xEAqBQzjwCAitt+++2zaNGivPnmm5UupaJqa2tz/vnn5/zzz8+f/vSn/OpXv8r111+fl156Kf/+7/+eefPm5eqrry7rnNtvv33918uWLWtyBtOyZcvqv95hhx3qv95uu+3y2muvrfN1aa51gj796U/nH//4R0aMGJHx48c32t7UWlAAQMvwzy8AQMXttddeSZK5c+cW7rNixYq88cYbGzT7phrts88++c///M/MmTMnp556apK3130qN6xZ29skefbZZ5vc553j79z//e9/f5LkueeeKzz/K6+8UlY9TXn11VfrH108/vjjN/p8AEDzEh4BAJvU2jVl3hn6LF26NLfcckv9mjonnXRSkuTBBx8sDEeOOOKI9O7de4v9RKwvfOELGT58eKPxmpqafPWrX03y9iyfRYsWNdjeVH8fffTR3HrrrUneXqh67SLQ9957b5PXXjvevXv3HHLIIfXjxx13XJJk2rRp9WsOvdPLL7+cOXPmrN8PuA7vrL0oHPz73/++0dcBADaM8AgA2KS6d++eJA1Cjz/96U856aST8oc//CFJ8slPfjK77757Fi1alB/+8IeNzjF58uQ89NBDGTly5AYvHL25W7JkSe6+++4sXLiw0bann346ydsLV6/9dLC1murvhRdemC9+8YtJknbt2uXyyy9Pknz/+9/P4sWLG113zJgxSd7+RLmampr6beecc066dOmSBQsW5LrrrmtU1ze/+c0yf8qmde3atX5B9JtvvrnR9nvuuWeds9IAgE3LmkcAsBV78cUXs3jx4rz44ov1Y2sfH+rYsWPhp3Ot9fTTT+ett97K0qVL68/35z//OTvvvHP9J48ddNBB6dSpU2bPnp0bbrghffr0yde//vV06NCh/iPi27dvn9tvvz3HHntsvvKVr+TFF1/MCSeckCSZOnVqrrzyyhx88MH1IUjy9iNuS5cubXTt3r17p2PHjvW1vbPWN954I/369XvXRZ6XLl3aKKxYe/7OnTunZ8+eTfbhtddeqz9+bR/XfkLb2nrX7vPKK6/kz3/+c7p165Zu3bqlVatWefXVVzN48OB8+ctfzvvf//6sXr06f/zjH3PFFVekXbt2ueaaa9KqVasG1z7uuOPyhz/8IT/+8Y/Ts2fPPPHEE5k8eXLOPffc+n3OPvvs/O1vf8uVV16ZQw89NF//+tfTt2/fzJkzJ5dffnn+/ve/59/+7d/y6U9/usG5u3btmptuuilDhw7NF77whbz00ks57rjjsmLFikyYMCF/+9vfctBBB2XatGmNeri2J2tnis2dOzddu3YtfF+NGTMmQ4cOza233ppPfOITGTlyZHbYYYc8+OCDueKKK9KxY8csXbq00XtsbZ/fed033ngje+65Z1577bW88MIL9Y/Wvfbaaw16vjHvEQDYqpQAgK3WiBEjSkma/DNo0KB3PX633XZr8thx48Y12G/q1Kmlgw46qNS+ffvSdtttV/rQhz5UmjRpUqPzLVmypHTZZZeVPvCBD5Q6dOhQqqmpKe2xxx6lyy67rLRs2bIG+w4aNKjJa0+ePHmdtc2dO/ddf67JkycX9mXEiBHr3Yd33moV1XvJJZeUSqVS6fXXXy/95Cc/KQ0dOrS06667ltq1a1dq165dqVevXqURI0aUZs2a1WStK1asKJ1//vmlnXbaqdS2bdvSLrvsUvr85z9fev3115v8uT72sY+VunfvXmrbtm2pW7dupWHDhpXuvffedfbj8ccfL51wwgmlHXfcsVRTU1N63/veV7r44otLy5cvLx166KGlJKUJEyasV0/W9b566KGHSh/5yEdKO+64Y6lNmzalbt26lYYOHVqaMmVKo/6tfY+tq+/jxo1bZ8835j0CAFuTVqXSVrLqJAAAzW7vvffOrFmzcs899+Too4+udDkAwCZgzSMAAApdc801eeSRR5rctnTp0jz99NNp06ZNBg4c2MKVAQAtRXgEAEChn/70p7n44oub/BS0H/zgB1mxYkXOOOOMdO7cuQLVAQAtwYLZAACs0z333JOPfexjOfvss9OzZ88sXrw4t956a6655prst99+9Z/WBgBsmax5BABAoVmzZuXXv/51Jk2alOeffz4LFixIbW1tdt9995x88sn57Gc/m9ra2kqXCQBsQlUVHu2+++6pq6trMDZv3rzU1tZm5syZSZKxY8fmmmuuSfv27dO+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vfZbs2ZNc14WAAAAgE2kWcOj7t27r9d+t956a3NeFgAAAIBNpFnDo/V10kknVeKyAAAAAJSpWdc8spYRAAAAwJalWcOj8ePHr9d+rVq1as7LAgAAALCJNPunrc2dO7fR2LJly/Liiy/m1ltvzQsvvJAxY8Y092UBAAAA2ASaNTw67rjjsttuuzW57f3vf3+OOuqojB07Ntdff30uvfTS5rw0AAAAAJtAsy6Yfccdd7zrPieddFJ+9rOfNedlAQAAANhEWvzT1ubNm5eFCxe29GUBAAAA2ADN+tja3//+98JtS5cuzVNPPZXLL788/fv3b87LAgAAALCJNGt41KtXr3f9JLWamprcfvvtzXlZAAAAADaRZv+0teHDhzcaa9WqVdq3b5/3vve9OfHEE7PLLrs092UBAAAA2ASaPTwaN25cc58SAAAAgApp1gWzJ0+e3JynAwAAAKDCmnXm0aBBg+q/XrNmTZ566qm89tpr6dy5c/r16/eu6yEBAAAAsHlp1plHSbJ8+fJ85StfSZcuXbLnnnvmkEMOyR577JEuXbrkggsuyPLly5v7kgAAAABsIs068+jNN9/MEUcckT/+8Y9p1apV6urq0rFjxyxdujTz58/Pt7/97UydOjX3339/amtrm/PSAAAAAGwCzTrz6Dvf+U5mz56dH//4x1m8eHFefPHFPP3003nxxRezaNGiXHPNNfnLX/6Sq666qjkvCwAAAMAm0qzh0c0335xx48bl05/+dLbffvsG23bYYYecc845+dnPfpabbrqpOS8LAAAAwCbSrOHRs88+mw9/+MPr3Oe4447L3Llzm/OyAAAAAGwizRoetW/fPgsWLFjnPi+//HI6dOjQnJcFAAAAYBNp1vDooIMOyr/9279l1apVTW5ftWpV/u3f/i2HHHJIc14WAAAAgE2kWT9t7Wtf+1oGDRqURx99NCeccEJ23333+k9bmz17dm699da88MILefDBB5vzsgAAAABsIs0aHh100EG5/vrr8+lPfzpXX311WrVqVb+tVCqlY8eOufHGG3PAAQc052UBAAAA2ESaNTxKktNOOy1HHHFErr/++kyfPj1LlixJp06d8sEPfjDDhw9Pt27dmvuSAAAAAGwizR4eJUldXV2++tWvbopTAwAAANCCNnrB7FGjRuWEE07ICSeckHHjxjW5z6uvvpr9998/EydO3NjLAQAAANCCNio8euyxx/Kzn/0s//M//5MFCxakZ8+eTe7Xrl27zJkzJ8cdd1zGjh27MZcEAAAAoAVtVHh05513pra2Nr/85S/z4IMP5phjjmlyv+222y7PP/98Pv/5z+fcc8/NY489tjGXBQAAAKCFbFR49OCDD+bzn/98TjrppHfdt0OHDvne976Xk046KT/4wQ825rIAAAAAtJCNCo/+8pe/ZPjw4WUdc/7552fq1Kkbc1kAAAAAWshGhUevvvpqevXqVdYx/fv3z0svvbQxlwUAAACghWxUeNShQ4esWbOmrGNKpVLatWu3MZcFAAAAoIVsVHjUt2/fPProo2Ud88gjj+S9733vxlwWAAAAgBayUeHRkCFDctlll6337KM1a9bkG9/4RoYOHboxlwUAAACghWxUeHTeeedl5syZGTZsWF588cV17vv8889n6NCh+fOf/5zzzjtvYy4LAAAAQAtpszEHd+nSJTfeeGM++tGPpnfv3hk0aFD233//7Lzzzmnfvn3efPPNvPDCC3nkkUcyderUtG7dOrfffns6d+7cXPUDAAAAsAltVHiUJMcdd1wmTpyY4cOH57777sv999/faJ9SqZTddtstN954Yw4++OCNvSQAAAAALWSjw6MkOeKIIzJnzpz84he/yD333JOnnnoqr7/+enbYYYe8733vy5AhQ3LyySenbdu2zXE5AAAAAFpIs4RHSdKuXbuMGDEiI0aMaK5TAgAAAFBhG7VgNgAAAABbNuERAAAAAIWERwAAAAAUEh4BAAAAUKjZFswGgE1t5JmjK11CRYy7YXSlSwAAYCtm5hEAAAAAhYRHAAAAABSqqvBo6tSpOeGEEzJ48OAceuih2WuvvfL973+/wT6333579t9//xx22GE5+OCDM3369ApVCwAAAFD9qmrNo5tuuin77LNPvv71rydJZs6cmf322y99+vTJ8ccfnxkzZuS0007LI488kv79++fOO+/MsccemyeeeCJ1dXUVrh4AAACg+lTVzKPzzjsv559/fv33H/jAB9KpU6f87W9/S5J885vfzLHHHpv+/fsnSY4//vh07949P/rRjypSLwAAAEC1q6rwqH///tluu+2SJGvWrMl1112XmpqanHTSSUmSe++9NwMHDmxwzP77759Jkya1eK0AAAAAW4KqCo/W+sY3vpGddtop3/ve93L3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tGmToUOH5uSTT85OO+2U73znO7njjjvSrl27vPbaa+nUqVOuvPLKDBw4sNLlV9TcuXMzcuTIPPzww6mrq0uvXr2ycuXKLF++PMcdd1zOP//8dO7cudJlblaa6tk7Pfzww1m+fHllittMvVvPnnzyydx8880ZPHhwRerbXC1fvjzf+973cvvtt6empiarVq3KqlWrctRRR+XUU0/NHnvsUekSK67cHr388svp3r17hardvBXdd/C2pn6PLVu2LAsWLMhBBx2UMWPG5D3veU+ly6y49bknW2vNmjU58MAD8/LLL+fZZ5+tXNGbgXL6liSvvPJKunXrVqFqNx/uY8vnPnYzUYJmMn/+/FK7du1KSUpz5sxpsG3AgAGlK664ov77a665plRbW1t64oknWrrMzcKyZctKBx54YOmMM84oLVu2rH78vvvuK3Xs2LH0hS98oVQqlUqdO3cuPfnkk/Xbv/SlL5Xe8573lFavXt3SJW+Wdtttt9Ill1xS//38+fNLH/3oR0u9evVq9B7kbf/cs3eO07Sino0YMaI0efLkFq9nc7Zs2bLSQQcdVDrllFNK//jHP+rHH3vssVLXrl1LJ598cgWr2zxsSI+uu+66liyxaqzrvoOG/vn32N///vdS586dS5/4xCcqV9RmYn3vydb6wQ9+UOrUqdNW//dmuX1buXJlady4cS1b5GbOfWz53MdWlsfWaDY333xzvvSlL2WbbbbJTTfd1GBb//79c95559V/f84556R169b5zW9+09JlbhZGjx6d5557Ltddd13at29fP37EEUfkq1/9av33v/vd79KvX7/67wcPHpwFCxbk9ddfb9F6q0X37t3zq1/9Kttvv31OP/30SpdTVa6//vpKl1B1vvzlL2fvvfeudBmbla9//et59tlnM27cuGy77bb14/vuu28uv/zyCla2+SinR7fddluSZOXKlZk8eXKL1lkN1nXfwbrtsssuGTx4cO69995Kl1Jx63tPliQvvvhifvrTn+bTn/50S5e52Vnfvq39PZYks2bNyrx581q0zmriPnbDuY9tGcIjms0vf/nLnH/++Tn88MMzYcKEBtt+/vOfN7hJbtWqVWpra7NixYqWLrPiVq9enWuvvTbDhg1LbW1to+3nnHNORowYkSQ54IAD6scXLVqUH//4xxk+fHg6derUUuVWnbZt2+Zzn/tcHnrooUyfPr3S5Wz2nn322QwePDiDBg2qdClVpVevXtlrr72y4447VrqUzcaqVavyk5/8pPB320knnVT/u21rVW6PnnvuuXz729/OFVdckUcffbQlS60K67rv4N2tXLky22yzdf9fgXLuyZLkvPPOy5VXXtkgLNkaldO3xYsX53//939zwgkn5O67786aNWtautyq4j62PO5jW9bW/TcGzeavf/1rtt9++3Tr1i2nn356Zs+enccee2yd+y9atCgnnXRSC1a5eXjyySezZMmS7L777k1u79atW/bdd9/671evXp0PfehD2WmnndK9e/dce+21LVVq1Vq7JtQjjzxS4Upg6/HUU0/l9ddfz/vf//4mt3fp0iVDhgxp4ao2L+X0aMyYMRk9enRefPHFfOpTn8pXvvKVlix1s1fufQcNTZ8+Pffdd18++9nPVrqUiirnnuyOO+5ImzZt8q//+q8tWeJmaX379vzzz+eHP/xhvvCFL+TUU0/NV7/61fTs2bOFq60+7mPZXAmPaBYTJkzIaaedliQ54YQTUltbu85/Bbz00kvzuc99Lh/4wAdaqsTNxmuvvZYkDWZirUvr1q3z8MMPZ8GCBVmyZEkOP/zwvPXWW5uwwuq3/fbbJ/m/XtPQ+PHjM3jw4AwePDinnHJKpcupCu/smQWym7b2v7eOHTtWtpDNWDk96tu3b6688sp8//vf93+2mlDufQf/93tswIABOeSQQ/KJT3xiqw8l1/ee7I033siFF16YMWPGbPqiqsD69m316tU5+OCDM3369K3yH4w3lPvYdXMfWznCI5rFb3/723zsYx9Lkuywww758Ic/nF/84hdNTk299tpr89JLL+W73/1uS5e5WVj76QlLly4t67jtttsu1157bf74xz/mxhtv3BSlbTGWLFmSJD6posBZZ52VKVOmZMqUKbn55psrXU5VeGfPpkyZUulyNktrH6d94403GozPmDEjgwcPzsCBAxt9OsrWppweHX/88WnXrl1Wr16dxx9/vNHaK1u7cu47eNva32MzZszIwoULs3Tp0nzgAx+o/ztza7S+92QXX3xxzj333PTo0aMlytrsrW/fevXqlf322y/J24/h/uAHP8isWbM2eX3Vzn3surmPrZw2lS6A6vfII49k3rx5+chHPlI/tnDhwsybNy/3339/jjrqqPrxW265JT//+c/z+9//Pm3btq1EuRXXr1+/dO7cObNnz17nfmvWrMmaNWvSps3//WfatWvXvOc978kTTzyxqcusamvXBvngBz9Y4Uo2f7169RKGbICt/eOZm9KvX79sv/32jX4/DRgwIFOmTMn48eMzcuTIClW3eSinRytWrMjEiRNz5ZVXpl27drnlllsqUfJmqZz7Dpq27bbb5qtf/Wr233///PKXv9xqF4Be33uy+++/PzNmzMivf/3rJG//HTB//vwMHjw4vXv3zrhx41qi3M3G+vYtSV5//fWMGDEif/jDH/LhD384/fv3b4EKq5v72PXnPrZlmXnERpswYUJ++tOfNvhX+RkzZqRTp04NppD/7ne/y7e+9a3ceeed2W677bJ48eL6v4S3Jq1bt87nPve53H777U0uGD5y5MiMGjUqU6dOzRe/+MUG25YvX55FixZ5hGEdVq5cmf/+7//OQQcdlAEDBlS6nKpx1113NZoNAeVo06ZNPvWpT+V//ud/yp5ZubUop0ennnpqFi9enDvvvDPnnXde3ve+97VQlZu/9b3vYN3W/uPUqlWrKlxJ5azvPdnMmTMzderU+vfbWWedlbq6ukyZMmWrC46S9e/bI488kosvvjhHHnlkZs2alX333TetW7euQMXVw33shnEf2zKER2yU1atX57777mu0eGBNTU1OOumk3HrrrXnzzTfzwAMP5Oyzz86ll16aZ555JtOnT8+0adNy1113Vajyyrr44ovTt2/ffPrTn87y5cvrx2+88cZMmjQpF198cZK3P0lmzpw5SZJSqZSvf/3r6dChQz7+8Y9XpO7N3csvv5xPfOITef311/0fiDJ95zvfycKFCytdBlXusssuS+/evTN8+PC8/vrr9eOLFy/OtGnT0qpVqwpWt3lY3x5NmDAhp5xySvr27Zu99967UuVudtb3voN1W7NmTcaOHZva2toceeSRlS6notb3noyG1qdve++9d8aMGZPhw4enXbt22XPPPStY8ebPfeyGcx/bMjy2xgZbsmRJjjnmmMybNy//7//9vwafAnbTTTdl6tSpef3113PIIYfk8ccfz6pVq3Lcccc1OMfW+rHN7dq1y8SJE3PVVVdl8ODBqa2tzcqVK9OnT5888MAD2W233bLddtvlU5/6VD7+8Y9n2223zfLly9OlS5fcf//92XXXXSv9I1TU3LlzM3LkyMyfPz/jx4/PlClT8tZbb2X58uU5/vjj87Of/cxz4v9kzpw5Oe200zJv3ryMHTs2v//97xts/8tf/lKhyjZfTb3PJkyYYObfOnTo0CGTJ0/O1VdfnSOPPDIdOnTIsmXLsmrVqhx00EE+DSvr36P27dvXB0keXXhbOfcdP/3pT7PPPvtUrtjNRFO/x9asWZNXX301Xbt2ze23355+/fpVusyKWp97srXmz5+fU045pcFja2eeeWbOPvvsCv4ElbG+fdtmm23q/3ftp4ht7dzHls997OahValUKlW6CAAAAAA2Tx5bAwAAAKCQ8AgAAACAQsIjAAAAAAoJjwAAAAAoJDwCAAAAoJDwCAAAAIBCwiMAAAAACgmPAAAAACgkPAIAAACgkPAIAAAAgELCIwCAd9GrV6+0atWqyT/t27fPHnvskdGjR2fZsmX1x0yePDmdOnXKN7/5zUbnGz9+fEaPHr3Rde2+++71dZx11lkbfT4AgKYIjwAA3sWzzz6bUqlU/32pVEqpVMrKlSszc+bMDBgwIJdeemkOPfTQ+gDplVdeyZIlS/L3v/+90fnGjx+fSy+9dKPrevLJJzN37tyNPg8AwLoIjwAANlCbNm3yvve9L+PHj89ee+2Vxx57LD/60Y+SJCeffHJeeuml+u8BAKqV8AgAYCNts802GTRoUJJk6tSp9eM9evTINtu43QIAqpu7GQCAZjZ48OD6tYgGDx5cPz5+/Pi0atUqDzzwQJI0WDtp/PjxDc5x991358gjj8wOO+yQbbfdNnvssUfOOOOM/O53vyu87h//+Mccdthh6dChQ7p27Zpzzjknb7755qb4EQGArYjwCABgI61Zs6Y+EBo0aFCmTJnSYI2ktc4666yUSqX6WUpr104qlUoNFry+6qqrctxxx6V///556qmnMm/evHz729/O/fffnw9/+MN57bXXGp37r3/9a0aPHp1rr702L730Ui666KL85Cc/yb//+79vkp8ZANh6tCo1dWcDAEAjrVq1SpL6YGj16tV55plncsUVV+TnP/959t1330ybNi0dOnSo339tmPROgwcPzgMPPNBkwPSnP/0pAwYMyIEHHphp06Y12HbLLbfkpJNOyuLFi9OpU6ckby/m3bt377Rt2zbPP/98unfvXr//nnvumRdffDGLFy9urhYAAFshM48AAMq09lGzNm3aZO+998706dPz9a9/vUFwtKF+8pOfZM2aNTnllFMabRsyZEhGjRqVmpqaRtsOOOCABsFRkvTr1y+vvfZaFixYsFE1AQBbtzaVLgAAoNpsyonbjz76aJK3g59/1rFjx1x33XVNHrfTTjs1Gtt2222TJMuWLWvGCgGArY2ZRwAAm5G16xl17NixrOPat2/faOyfH7MDANgQwiMAgM3I2rWMli5dWtlCAAD+f8IjAIAWtnZGUFM++MEPJkmefPLJRtveeOONjBkzJn/72982WW0AAP9MeAQA0MI6d+6cJFm+fHmS5Lvf/W5OPPHEJMk555yTbbbZJr/85S8bHferX/0qX/nKV+pnJwEAtAThEQBACxs4cGCS5N57783ChQtz/fXXZ7vttkuS7L333vnWt76V//3f/83nP//5zJ8/P//4xz/ym9/8Jl/+8pdz+eWXp2vXrpUsHwDYyrQqWUERAGCdevXqleeee67R+OTJkzN48OBG44MHD84DDzzQYOySSy7J6NGjkyT/+Mc/cu6552bixIlZuXJlDj300PzkJz9p8Ilpd999d6666qrMmDEjpVIp/fr1yxe/+MWcfvrp67zO5MmTkySHH354g/FBgwZlypQp5fzYAABJhEcAAAAArIPH1gAAAAAoJDwCAAAAoJDwCAAAAIBCwiMAAAAACgmPAAAAACgkPAIAAACgkPAIAAAAgELCIwAAAAAKCY8AAAAAKCQ8AgAAAKCQ8AgAAACAQsIjAAAAAAoJjwAAAAAoJDwCAAAAoJDwCAAAAIBCwiMAAAAACgmPAAAAACj0/wHOxxhEVtNWpgAAAABJRU5ErkJggg==\n",
- "text/plain": [
- "