diff --git a/.coveragerc b/.coveragerc index a134e125e..e5a0bef7f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,10 +3,16 @@ source = music21/ omit = - music21/test/timeGraph* + music21/test/* + music21/configure.py + music21/figuredBass/examples.py + music21/alpha/* + dist/dist.py [report] exclude_lines = + import music21 + music21.mainTest() if TYPE_CHECKING: if t.TYPE_CHECKING: if __name__ == .__main__.: diff --git a/.github/workflows/maincheck.yml b/.github/workflows/maincheck.yml index d046ccaec..45ece1636 100644 --- a/.github/workflows/maincheck.yml +++ b/.github/workflows/maincheck.yml @@ -9,6 +9,11 @@ on: branches: - '*' +env: + # We run coverage on a middle supported Python so that failures on newest and + # oldest versions return first. + PY_VERSION_WITH_COVERAGE: '3.13' + jobs: run_tests: runs-on: ubuntu-latest @@ -35,10 +40,14 @@ jobs: - name: Setup Lilypond run: uv run python -c 'from music21 import environment; environment.UserSettings()["lilypondPath"] = "/home/runner/bin/lilypond"' - name: Run Main Test script + if: ${{ matrix.python-version != env.PY_VERSION_WITH_COVERAGE }} run: uv run python -c 'from music21.test.testSingleCoreAll import ciMain as ci; ci()' + - name: Run Main Test script with coverage + if: ${{ matrix.python-version == env.PY_VERSION_WITH_COVERAGE }} + run: uv run coverage run -m music21.test.testSingleCoreAll ci - name: Coveralls - if: ${{ matrix.python-version == '3.13' }} - env: # when changing number above also change coverageM21.getCoverage + if: ${{ matrix.python-version == env.PY_VERSION_WITH_COVERAGE }} + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_SERVICE_NAME: github run: uv run coveralls diff --git a/music21/key.py b/music21/key.py index b23054f51..05a2d69a0 100644 --- a/music21/key.py +++ b/music21/key.py @@ -1390,12 +1390,10 @@ def testNonTraditional(self): ''' AI-assisted (Claude). ''' - from music21 import key - - ks = key.KeySignature(3) + ks = KeySignature(3) self.assertFalse(ks.isNonTraditional) - ks = key.KeySignature() + ks = KeySignature() ks.isNonTraditional = True ks.alteredPitches = [pitch.Pitch('E`')] self.assertEqual(repr(ks), '') @@ -1403,7 +1401,7 @@ def testNonTraditional(self): # a non-traditional key signature is not equal to the C-major signature # it shares a `sharps` count with. - self.assertNotEqual(ks, key.KeySignature()) + self.assertNotEqual(ks, KeySignature()) def testSharpsNoneDeprecated(self): ''' diff --git a/music21/test/commonTest.py b/music21/test/commonTest.py index d473a02ab..64e06d18c 100644 --- a/music21/test/commonTest.py +++ b/music21/test/commonTest.py @@ -16,9 +16,7 @@ import copy import doctest import importlib -import importlib.util import os -import sys import typing import types import unittest.runner @@ -61,28 +59,6 @@ def testCopyAll(testInstance: unittest.TestCase, globals_: typing.Dict[str, typi testInstance.fail(f'Could not deepcopy obj {part}: {e}') -def load_source(name: str, path: str) -> types.ModuleType: - ''' - Replacement for deprecated imp.load_source() - - Thanks to: - https://github.com/epfl-scitas/spack for pointing out the - important missing "spec.loader.exec_module(module)" line. - ''' - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise FileNotFoundError(f'No such file or directory: {path!r}') - if name in sys.modules: - module = sys.modules[name] - else: - module = importlib.util.module_from_spec(spec) - if module is None: - raise FileNotFoundError(f'No such file or directory: {path!r}') - sys.modules[name] = module - spec.loader.exec_module(module) - - return module - # noinspection PyPackageRequirements def testImports(): ''' @@ -417,11 +393,11 @@ def getModule(self, fp, restoreEnvironmentDefaults=False): if skip: return None - name = self._getNamePeriod(fp, addM21=False) + name = self._getNamePeriod(fp, addM21=True) try: with warnings.catch_warnings(): - mod = load_source(name, fp) + mod = importlib.import_module(name) except Exception as excp: # pylint: disable=broad-exception-caught environLocal.warn(['failed import:', name, '\t', fp, '\n', '\tEXCEPTION:', str(excp).strip()]) diff --git a/music21/test/coverageM21.py b/music21/test/coverageM21.py deleted file mode 100644 index 51f7d10fd..000000000 --- a/music21/test/coverageM21.py +++ /dev/null @@ -1,70 +0,0 @@ -# ------------------------------------------------------------------------------ -# Name: coverageM21.py -# Purpose: Starts Coverage w/ default arguments -# -# Authors: Christopher Ariza -# Michael Scott Asato Cuthbert -# -# Copyright: Copyright © 2014-15 Michael Scott Asato Cuthbert -# License: BSD, see license.txt -# ------------------------------------------------------------------------------ -from __future__ import annotations - -import sys - -omit_modules = [ - 'dist/dist.py', - 'music21/test/*', - 'music21/configure.py', - 'music21/figuredBass/examples.py', - 'music21/alpha/*', -] - -# THESE ARE NOT RELEVANT FOR coveralls.io -- edit .coveragerc to change that -exclude_lines = [ - r'\s*import music21\s*', - r'\s*music21.mainTest\(\)\s*', - r'.*#\s*pragma:\s*no cover.*', - r'class TestExternal.*', - r'class TestSlow.*', - r'\s*if TYPE_CHECKING:\s*', - r'\s*if t.TYPE_CHECKING:\s*', -] - - -def getCoverage(overrideVersion=False): - # MEMORY / NOTE FOR UPDATING PYTHON: - # Run this on a MIDDLE supported Python version so that we can - # check timing of newest vs oldest, AND so that - # we can quickly see failures on newest and oldest. - # (The odds of a failure on the middle version are low if - # the newest and oldest are passing.) - # - # Note the .minor == 13 -- that makes it only run on 3.13 - # - # When changing the version, be sure also to change - # .github/workflows/maincheck.yml's line: - # if: ${{ matrix.python-version == '3.13' }} - if overrideVersion or sys.version_info.minor == 13: - try: - # noinspection PyPackageRequirements - import coverage # type: ignore - cov = coverage.Coverage(omit=omit_modules) # , debug='trace') - for e in exclude_lines: - cov.exclude(e, which='exclude') - cov.start() - import music21 # pylint: disable=unused-import # noqa: F401 - except ImportError: - cov = None - else: - cov = None - return cov - -def startCoverage(cov): - if cov is not None: - cov.start() - -def stopCoverage(cov): - if cov is not None: - cov.stop() - cov.save() diff --git a/music21/test/testSingleCoreAll.py b/music21/test/testSingleCoreAll.py index 6fd06d6dd..547416de2 100644 --- a/music21/test/testSingleCoreAll.py +++ b/music21/test/testSingleCoreAll.py @@ -25,17 +25,11 @@ from music21 import environment from music21.test import commonTest -from music21.test import coverageM21 from music21.test import testRunner environLocal = environment.Environment('test.testSingleCoreAll') -# this is designed to be None for all but one system and a Coverage() object -# for one system. -cov = coverageM21.getCoverage() - - def main(testGroup: Sequence[str] = ('test',), restoreEnvironmentDefaults=False, limit: bool|None = None, @@ -118,8 +112,6 @@ def main(testGroup: Sequence[str] = ('test',), runner = unittest.TextTestRunner(verbosity=verbosity) finalTestResults = runner.run(s1) - coverageM21.stopCoverage(cov) - if (finalTestResults.errors or finalTestResults.failures or finalTestResults.unexpectedSuccesses): @@ -136,18 +128,17 @@ def ciMain(): # and TestExternal (without doctests) with show=False # exits with the aggregated returnCode returnCodeTest = main(testGroup=('test',), verbosity=1) - # restart coverage if running main() twice - coverageM21.startCoverage(cov) returnCodeExternal = main(testGroup=('external',), verbosity=1, show=False) sys.exit(returnCodeTest + returnCodeExternal) # ------------------------------------------------------------------------------ if __name__ == '__main__': - # if optional command line arguments are given, assume they are - # test group arguments - if len(sys.argv) >= 2: - unused_returnCode = main(sys.argv[1:]) + # 'ci' runs what GitHub Actions runs; other arguments are test group names. + if len(sys.argv) < 2: + main() + elif sys.argv[1] == 'ci': + ciMain() else: - unused_returnCode = main() + main(sys.argv[1:])