Skip to content

Commit

Permalink
fix: broken type checks as of Live 12.1 (#7)
Browse files Browse the repository at this point in the history
Live 12.1 switched to python 3.11, which is currently impractical to
decompile. See
gluon/AbletonLive12_MIDIRemoteScripts#2 for
more discussion.

`pycdc` does decompile some basic structural elements of the Live
libraries, but outputs mostly empty function and class definitions.
https://pylingual.io/ seems to work better but isn't currently possible
to integrate in an automated way.

This PR adds https://github.com/gluon/AbletonLive12_MIDIRemoteScripts
(currently not updated for 12.1 but seems to work fine for our use
cases) as a submodule and sets it up for use with the typechecker.
  • Loading branch information
kmontag authored Dec 10, 2024
1 parent e4b787d commit 26b748c
Show file tree
Hide file tree
Showing 19 changed files with 174 additions and 265 deletions.
51 changes: 12 additions & 39 deletions .github/workflows/validate_and_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
- uses: awalsh128/cache-apt-pkgs-action@latest
Expand All @@ -17,63 +18,35 @@ jobs:
version: 1.0
- uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: 3.11
cache: pip

- run: python -m pip install poetry
- run: make install
- run: make lint

check-types:
runs-on: macos-latest
timeout-minutes: 30
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master

- uses: actions/checkout@v3
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libasound2-dev
version: 1.0
- uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: 3.11
cache: pip

- run: python -m pip install poetry
- run: make install

# LLM_TODO: The above cacheable actions create and populate the
# "__ext__/System_MIDIRemoteScripts/" directory. This content can be cached based
# on the Ableton Live version, which can be retrieved using `brew info --cask
# --json=v2 ableton-live-lite | jq -r '.casks[0].version'`. Rewrite the actions so
# that they only run if necessary, and cache the generated decompilation
# directory between runs.

# We can cache the decompiled remote scripts based on the Live version, to avoid
# the expensive installation/decompilation process if possible.
- name: Get Ableton Live version
id: get-ableton-live-version
run: echo "version=$(brew info --cask --json=v2 ableton-live-lite | jq -r '.casks[0].version')" >> $GITHUB_OUTPUT

- name: Cache Ableton Live decompilation
uses: actions/cache@v3
id: cache-decompilation
with:
path: __ext__/System_MIDIRemoteScripts/
key: ableton-live-decompilation-${{ steps.get-ableton-live-version.outputs.version }}

- name: Install Ableton Live
if: steps.cache-decompilation.outputs.cache-hit != 'true'
run: brew install --cask ableton-live-lite

- name: Decompile system remote scripts libraries
if: steps.cache-decompilation.outputs.cache-hit != 'true'
run: make decompile

# Run the command manually (not using `make`) to avoid failures if Live isn't installed.
- run: poetry run pyright .
- run: make check

release:
name: Publish release to GitHub
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency: release
environment:
name: release
Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ __pycache__/
# Virtual environment.
/.venv/

# Decompiled system scripts.
/__ext__/System_MIDIRemoteScripts/

# Markers for Makefile tasks which would otherwise have no artifacts.
/.make.*

Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "__ext__/AbletonLive12_MIDIRemoteScripts"]
path = __ext__/AbletonLive12_MIDIRemoteScripts
url = https://github.com/gluon/AbletonLive12_MIDIRemoteScripts.git
29 changes: 6 additions & 23 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ default: lint check
.PHONY: install
install: .make.install

.PHONY: decompile
decompile: __ext__/System_MIDIRemoteScripts/.make.decompile

.PHONY: lint
lint: .make.install
$(POETRY) run ruff format --check .
Expand All @@ -28,7 +25,7 @@ format: .make.install
$(POETRY) run ruff check --fix .

.PHONY: check
check: .make.install __ext__/System_MIDIRemoteScripts/.make.decompile
check: .make.install __ext__/AbletonLive12_MIDIRemoteScripts/README.md
$(POETRY) run pyright .

.PHONY: test
Expand All @@ -41,32 +38,18 @@ img: .make.install

.PHONY: clean
clean:
rm -rf __ext__/System_MIDIRemoteScripts/
# The .venv folder gets created by poetry (because virtualenvs.in-project is enabled).
rm -rf .venv/
rm -f .make.install
rm -f .make.*

# Proxy target for the remote scripts submodule.
__ext__/AbletonLive12_MIDIRemoteScripts/README.md: .gitmodules
git submodule update --init "$(@D)"

# Set files with different configurations for testing.
$(TEST_PROJECT_DIR)/%.als: .make.install $(TEST_PROJECT_DIR)/create_set.py
$(POETRY) run python $(TEST_PROJECT_DIR)/create_set.py $*

__ext__/System_MIDIRemoteScripts/.make.decompile: $(SYSTEM_MIDI_REMOTE_SCRIPTS_DIR) | .make.install
# Sanity check before rm'ing.
@if [ -z "$(@D)" ]; then \
echo "Sanity check failed: compile dir is not set"; \
exit 1; \
fi
rm -rf $(@D)/
mkdir -p $(@D)/ableton/
@if [ -z $(SYSTEM_MIDI_REMOTE_SCRIPTS_DIR) ]; then \
echo "System remote scripts directory not found" ; \
exit 1; \
fi
@if [ ! -d $(SYSTEM_MIDI_REMOTE_SCRIPTS_DIR) ]; then \
echo "The specified remote scripts directory ("$(SYSTEM_MIDI_REMOTE_SCRIPTS_DIR)") does not exist"; \
exit 1; \
fi

# decompyle3 works for most files, and the ones where it doesn't don't
# matter for our purposes.
$(POETRY) run decompyle3 -r -o $(@D)/ableton/ $(SYSTEM_MIDI_REMOTE_SCRIPTS_DIR)/ableton/
Expand Down
1 change: 1 addition & 0 deletions __ext__/AbletonLive12_MIDIRemoteScripts
4 changes: 2 additions & 2 deletions control_surface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from contextlib import contextmanager
from functools import partial

from ableton.v3.base import const, depends, inject, listens, task
from ableton.v3.base import const, depends, inject, task
from ableton.v3.control_surface import (
ControlSurface,
ControlSurfaceSpecification,
Expand All @@ -30,7 +30,7 @@
from .display import display_specification
from .elements import NUM_GRID_COLS, NUM_ROWS, Elements
from .hardware import HardwareComponent
from .live import lazy_attribute
from .live import lazy_attribute, listens
from .mappings import (
DISABLED_MODE_NAME,
STANDALONE_INIT_MODE_NAME,
Expand Down
3 changes: 2 additions & 1 deletion control_surface/channel_strip.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging
from enum import Enum

from ableton.v3.base import listens
from ableton.v3.control_surface.components.channel_strip import (
ChannelStripComponent as ChannelStripComponentBase,
)
from ableton.v3.control_surface.controls import MappedControl
from ableton.v3.live import liveobj_valid

from .live import listens

logger = logging.getLogger(__name__)


Expand Down
3 changes: 2 additions & 1 deletion control_surface/clip_slot.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from logging import getLogger
from typing import Optional

from ableton.v3.base import depends, listens
from ableton.v3.base import depends
from ableton.v3.control_surface.components import (
ClipSlotComponent as ClipSlotComponentBase,
)

from .configuration import Configuration
from .live import listens
from .types import ClipSlotAction

logger = getLogger(__name__)
Expand Down
10 changes: 9 additions & 1 deletion control_surface/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from typing import Any, Dict, Optional, Union

from ableton.v3.control_surface.display import (
DefaultNotifications,
DefaultNotifications as __DefaultNotifications,
)
from ableton.v3.control_surface.display import (
DisplaySpecification,
Event,
State,
Expand Down Expand Up @@ -106,6 +108,12 @@ def _slider_value_notification(value: str):
return NotificationData(text=_right_align("", value), flash_on_repeat=False)


# The type-checker gets confused by the notifications inheritance structure, and thinks
# inner classes like `DefaultNotifications.Clip` are undefined. Just force it to ignore
# such checks by assigning to an `Any`.
DefaultNotifications: Any = __DefaultNotifications


class Notifications(DefaultNotifications):
class Clip(DefaultNotifications.Clip):
quantize = _quantize_notification
Expand Down
4 changes: 2 additions & 2 deletions control_surface/elements/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ableton.v2.control_surface.defaults import (
TIMER_DELAY,
)
from ableton.v3.base import clamp, depends, flatten
from ableton.v3.base import clamp, depends
from ableton.v3.control_surface import (
MIDI_CC_TYPE,
ControlElement,
Expand All @@ -31,7 +31,7 @@
)

from .. import sysex
from ..live import lazy_attribute
from ..live import flatten, lazy_attribute
from ..types import KeySafetyStrategy, TypedDict
from .button import LightedButtonElement
from .display import DisplayElement
Expand Down
4 changes: 2 additions & 2 deletions control_surface/elements/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from ableton.v2.control_surface import MIDI_INVALID_TYPE
from ableton.v2.control_surface.elements import ButtonElementMixin
from ableton.v3.base import EventObject, listens, memoize, task
from ableton.v3.base import EventObject, task
from ableton.v3.control_surface.elements import ButtonElement, Color
from ableton.v3.control_surface.midi import CC_STATUS

from ..colors import OFF, ColorInterfaceMixin, Skin
from ..live import lazy_attribute
from ..live import lazy_attribute, listens, memoize
from .compound import TransitionalProcessedValueElement

logger = getLogger(__name__)
Expand Down
4 changes: 2 additions & 2 deletions control_surface/elements/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

from ableton.v2.base import linear
from ableton.v2.control_surface.defaults import TIMER_DELAY
from ableton.v3.base import clamp, listens, nop, task
from ableton.v3.base import clamp, nop, task
from ableton.v3.control_surface import InputControlElement
from ableton.v3.control_surface.display import Renderable

from ..live import lazy_attribute
from ..live import lazy_attribute, listens
from ..xy import get_xy_value
from .light import LightedTransitionalProcessedValueElement

Expand Down
13 changes: 12 additions & 1 deletion control_surface/live.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# This file exports elements of the Live API for which we want to
# provide more specific types than the ones inferred by the type
# checker. Types are specified in the associated .pyi file.
from ableton.v3.base import lazy_attribute # noqa: F401
#
# Note the type-checker sees some of these as missing imports due to issues in the
# decompiled types, but in practice they're available.
#
# type: ignore
from ableton.v3.base import (
find_if, # noqa: F401
flatten, # noqa: F401
lazy_attribute, # noqa: F401
listens, # noqa: F401
memoize, # noqa: F401
)
12 changes: 12 additions & 0 deletions control_surface/live.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import typing

from ableton.v2.base import Slot as __Slot

T = typing.TypeVar("T")

def find_if(
predicate: typing.Callable[[T], typing.Any], seq: typing.Iterable[T]
) -> typing.Optional[T]: ...
def flatten(list: typing.Iterable[typing.Iterable[T]]) -> typing.Iterable[T]: ...

class lazy_attribute(typing.Generic[T]):
def __init__(self, func: typing.Callable[[typing.Any], T], name=...) -> None: ...
def __get__(self, obj, cls=...) -> T: ...

def listens(
event_path: str, *a, **k
) -> typing.Callable[[typing.Callable[..., typing.Any]], __Slot]: ...
def memoize(function: typing.Callable[..., T]) -> typing.Callable[..., T]: ...
4 changes: 2 additions & 2 deletions control_surface/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)

from ableton.v2.control_surface.mode import SetAttributeMode
from ableton.v3.base import depends, find_if, memoize
from ableton.v3.base import depends
from ableton.v3.control_surface import Component, ControlSurface
from ableton.v3.control_surface.component_map import ComponentMap
from ableton.v3.control_surface.layer import Layer
Expand All @@ -30,7 +30,7 @@
)

from .elements import NUM_COLS, NUM_GRID_COLS, NUM_ROWS
from .live import lazy_attribute
from .live import find_if, lazy_attribute, memoize
from .mode import (
DISABLED_MODE_NAME,
MODE_SELECT_MODE_NAME,
Expand Down
3 changes: 2 additions & 1 deletion control_surface/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from functools import partial
from time import time

from ableton.v3.base import depends, listenable_property, memoize
from ableton.v3.base import depends, listenable_property
from ableton.v3.control_surface.controls import ButtonControl
from ableton.v3.control_surface.mode import (
CallFunctionMode,
Expand All @@ -17,6 +17,7 @@
from ableton.v3.control_surface.mode import ModesComponent as ModesComponentBase

from .hardware import HardwareComponent
from .live import memoize
from .types import MainMode

if typing.TYPE_CHECKING:
Expand Down
Loading

0 comments on commit 26b748c

Please sign in to comment.