Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ if something is broken or missing for you, open an
updater would fetch and install a new version for you.
- **Progress inside a large file.** A long transfer of one big file shows
only elapsed time; a percentage and the bytes moved would show how far along it is.
- **`omip` in the editor and the browser.** **Install Package** and package search
offer `mpftp omip` for OmniPython packages, as the command line does.
- **Translations.** The browser interface and the extension in your
language, with English as the fallback for anything not yet translated.
2 changes: 1 addition & 1 deletion cli/src/mpftp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3000,7 +3000,7 @@ def build_parser() -> argparse.ArgumentParser:

srch = sub.add_parser(
"search",
help="Find a package in micropython-lib, the PyDevices index or the CircuitPython bundles",
help="Find a package in micropython-lib, the OmniPython or PyDevices index, or the CircuitPython bundles",
)
srch.add_argument("words", nargs="+", help="Words from the package's name or description")
srch.add_argument(
Expand Down
18 changes: 17 additions & 1 deletion cli/src/mpftp/package_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

"""Find a package without knowing its exact name.

One search across the three places a board's packages come from:
One search across the four places a board's packages come from:

* micropython-lib, through the index ``mip`` itself reads
(``https://micropython.org/pi/v2/index.json``);
* the OmniPython index (``https://lib.omnipython.org/index.json``), whose
packages ``omip`` installs along with what they need from micropython-lib;
* the PyDevices index (``https://PyDevices.github.io/mip/index.json``),
which carries the PyDevices packages and a mirror of micropython-lib so
their named dependencies resolve from one place;
Expand Down Expand Up @@ -36,6 +38,7 @@
from typing import Any, Callable, Optional

MIP_INDEX = "https://micropython.org/pi/v2"
OMNIPYTHON_INDEX = "https://lib.omnipython.org"
PYDEVICES_INDEX = "https://PyDevices.github.io/mip"

CACHE_ROOT = Path.home() / ".mpftp" / "package-cache"
Expand All @@ -52,6 +55,13 @@
"kind": "mip",
"url": MIP_INDEX + "/index.json",
},
{
"key": "omnipython",
"label": "OmniPython",
"interpreter": "micropython",
"kind": "mip",
"url": OMNIPYTHON_INDEX + "/index.json",
},
{
"key": "pydevices",
"label": "PyDevices",
Expand Down Expand Up @@ -139,6 +149,12 @@ def _install_for(source: dict[str, str], name: str) -> dict[str, Any]:
"method": "circup_install",
"params": {"packages": [name]},
}
if source["key"] == "omnipython":
return {
"command": f"mpftp omip {name}",
"method": "omip_install",
"params": {"packages": [name]},
}
if source["key"] == "pydevices":
return {
"command": f"mpftp mip {name} --index {PYDEVICES_INDEX}",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/mpftp/webui/app.css

Large diffs are not rendered by default.

70 changes: 35 additions & 35 deletions cli/src/mpftp/webui/app.js

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions cli/tests/fixtures/package-index/omnipython.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"packages": [
{
"author": "",
"description": "Color palette toolkit for PyDevices (wheel, cube, material_design)",
"license": "MIT",
"name": "palettes",
"path": "palettes",
"version": "0.0.15",
"versions": {
"6": [
"0.0.15"
],
"py": [
"0.0.15"
]
}
},
{
"author": "",
"description": "Cross-platform widget toolkit for PyDevices",
"license": "MIT",
"name": "pdwidgets",
"path": "pdwidgets",
"version": "0.0.25",
"versions": {
"6": [
"0.0.25"
],
"py": [
"0.0.25"
]
}
},
{
"author": "",
"description": "Lightweight Spotify Web API client for CPython, MicroPython and CircuitPython",
"license": "MIT",
"name": "sayso",
"path": "sayso",
"version": "0.1.2.dev0",
"versions": {
"6": [
"0.1.2.dev0"
],
"py": [
"0.1.2.dev0"
]
}
},
{
"author": "",
"description": "ST7789 display driver",
"kind": "driver",
"license": "MIT",
"name": "st7789",
"path": "st7789",
"tested": true,
"version": "0.1.0",
"versions": {
"6": [
"0.1.0"
],
"py": [
"0.1.0"
]
}
}
],
"updated": 1794234222,
"v": 2
}
53 changes: 39 additions & 14 deletions cli/tests/test_package_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
#
# SPDX-License-Identifier: MIT

"""`mpftp search`: one search across micropython-lib, the PyDevices index and the CircuitPython bundles.
"""`mpftp search`: one search across micropython-lib, the OmniPython and PyDevices indexes and the CircuitPython bundles.

The index samples under fixtures/package-index are cut from the real ones
(micropython.org/pi/v2, PyDevices.github.io/mip and the two bundles' release
manifests), so nothing here touches the network.
(micropython.org/pi/v2, lib.omnipython.org, PyDevices.github.io/mip and the
two bundles' release manifests), so nothing here touches the network.
"""

from __future__ import annotations
Expand All @@ -29,6 +29,7 @@

TEXT = {
SOURCE["micropython-lib"]["url"]: (FIXTURES / "micropython-lib.json").read_text(),
SOURCE["omnipython"]["url"]: (FIXTURES / "omnipython.json").read_text(),
SOURCE["pydevices"]["url"]: (FIXTURES / "pydevices.json").read_text(),
ps.bundle_manifest_url(SOURCE["adafruit"], "20261010"): (
FIXTURES / "adafruit-bundle.json"
Expand Down Expand Up @@ -182,6 +183,27 @@ def test_words_from_the_pypi_name_count(self):
hits = ps.rank(self.entries(), "circuitpython-bus-device")
self.assertEqual([h["name"] for h in hits], ["adafruit_bus_device"])

def test_omnipython_entries_install_with_omip(self):
entries = ps.parse_mip_index(TEXT[SOURCE["omnipython"]["url"]], SOURCE["omnipython"])
by_name = {e["name"]: e for e in entries}
self.assertEqual(sorted(by_name), ["palettes", "pdwidgets", "sayso", "st7789"])
pal = by_name["palettes"]
self.assertEqual(pal["source_label"], "OmniPython")
self.assertEqual(pal["interpreter"], "micropython")
self.assertEqual(pal["install"]["command"], "mpftp omip palettes")
self.assertEqual(pal["install"]["method"], "omip_install")
self.assertEqual(pal["install"]["params"], {"packages": ["palettes"]})

def test_a_package_on_omnipython_and_pydevices_lists_from_both(self):
# The two indexes carry different versions (OmniPython the newer), and
# each installs its own way, so neither hides the other.
with tempfile.TemporaryDirectory() as tmp:
res = run_search("pdwidgets", Net(), Path(tmp))
self.assertEqual(
[(h["source"], h["version"], h["install"]["method"]) for h in res["results"]],
[("omnipython", "0.0.25", "omip_install"), ("pydevices", "0.0.24", "mip_install")],
)

def test_mirrored_micropython_lib_packages_list_once(self):
entries = ps.parse_mip_index(
TEXT[SOURCE["micropython-lib"]["url"]], SOURCE["micropython-lib"]
Expand All @@ -207,25 +229,26 @@ def setUp(self):
def test_first_search_fetches_every_index_and_writes_the_cache(self):
net = Net()
res = run_search("neopixel", net, self.root)
self.assertEqual([s["from"] for s in res["sources"]], ["network"] * 4)
self.assertEqual([s["from"] for s in res["sources"]], ["network"] * 5)
self.assertFalse(res["offline"])
self.assertIsNone(res["note"])
self.assertEqual(sorted(p.name for p in self.root.iterdir()),
["adafruit.json", "community.json", "micropython-lib.json", "pydevices.json"])
self.assertEqual(len(net.fetched), 6) # four indexes, two latest-tag lookups
self.assertEqual(res["sources"][2]["tag"], "20261010")
["adafruit.json", "community.json", "micropython-lib.json",
"omnipython.json", "pydevices.json"])
self.assertEqual(len(net.fetched), 7) # five indexes, two latest-tag lookups
self.assertEqual(res["sources"][3]["tag"], "20261010")

def test_second_search_reads_the_cache_and_refresh_fetches_again(self):
net = Net()
run_search("neopixel", net, self.root)
net.fetched.clear()
res = run_search("neopixel", net, self.root)
self.assertEqual(net.fetched, [])
self.assertEqual([s["from"] for s in res["sources"]], ["cache"] * 4)
self.assertEqual([s["from"] for s in res["sources"]], ["cache"] * 5)
self.assertEqual([h["name"] for h in res["results"]], ["neopixel", "neopixel"])
res = run_search("neopixel", net, self.root, refresh=True)
self.assertEqual(len(net.fetched), 6)
self.assertEqual([s["from"] for s in res["sources"]], ["network"] * 4)
self.assertEqual(len(net.fetched), 7)
self.assertEqual([s["from"] for s in res["sources"]], ["network"] * 5)

def test_an_old_cache_is_fetched_again(self):
net = Net()
Expand All @@ -234,8 +257,8 @@ def test_an_old_cache_is_fetched_again(self):
old = ps.INDEX_TTL_SEC + 60
with mock.patch("mpftp.package_search.time.time", return_value=ps.time.time() + old):
res = run_search("neopixel", net, self.root)
self.assertEqual(len(net.fetched), 6)
self.assertEqual([s["from"] for s in res["sources"]], ["network"] * 4)
self.assertEqual(len(net.fetched), 7)
self.assertEqual([s["from"] for s in res["sources"]], ["network"] * 5)

def test_offline_with_an_old_cache_searches_it_and_says_so(self):
run_search("neopixel", Net(), self.root)
Expand All @@ -257,7 +280,7 @@ def test_offline_with_no_cache_is_a_clear_message_and_no_results(self):
res = run_search("neopixel", Net(down=True), self.root)
self.assertTrue(res["offline"])
self.assertEqual(res["results"], [])
self.assertIn("Could not fetch micropython-lib, PyDevices, Adafruit CircuitPython Bundle, "
self.assertIn("Could not fetch micropython-lib, OmniPython, PyDevices, Adafruit CircuitPython Bundle, "
"CircuitPython Community Bundle and there is no cached copy yet", res["note"])
self.assertEqual(list(self.root.iterdir()), [])

Expand All @@ -283,7 +306,7 @@ def test_a_damaged_cache_file_is_fetched_over(self):
(self.root / "adafruit.json").write_text("not json")
net.fetched.clear()
res = run_search("neopixel", net, self.root)
self.assertEqual(res["sources"][2]["from"], "network")
self.assertEqual(res["sources"][3]["from"], "network")
self.assertEqual(len(net.fetched), 2)


Expand Down Expand Up @@ -327,6 +350,8 @@ def test_text_output_shows_name_version_source_and_the_install_line(self):
self.assertEqual(
text.splitlines(),
[
"pdwidgets 0.0.25 [OmniPython] — Cross-platform widget toolkit for PyDevices",
" mpftp omip pdwidgets",
"pdwidgets 0.0.24 [PyDevices] — Cross-platform widget toolkit for PyDevices",
" mpftp mip pdwidgets --index https://PyDevices.github.io/mip",
],
Expand Down
19 changes: 11 additions & 8 deletions docs/guides/install-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ mpftp omip NAME # MicroPython; an OmniPython package
mpftp circup adafruit_display_text # CircuitPython; installs to /lib
```

In the editor and the browser interface, **mpftp: Install Package** runs `mip`
or `circup`. `omip` is on the command line.
In the editor and the browser interface, **mpftp: Install Package** asks
whether to use `omip` or `mip` on MicroPython, then for the name; on
CircuitPython it runs `circup`.

## Install an OmniPython package

Expand Down Expand Up @@ -62,12 +63,14 @@ adafruit_display_text 5.0.6 [Adafruit CircuitPython Bundle] — Displays text u
mpftp circup adafruit_display_text
```

Search needs no board. It covers micropython-lib, the PyDevices index and the
CircuitPython bundles (Adafruit and community); with a board connected it keeps
to the indexes that board installs from. In the editor it is **mpftp: Search
Packages…**, and also in the ⋯ menu of the File Transfer view and the browser
interface. A PyDevices package's install line carries
`--index https://PyDevices.github.io/mip`, which `mip` needs to find it.
Search needs no board. It covers micropython-lib, the OmniPython index, the
PyDevices index and the CircuitPython bundles (Adafruit and community); with a
board connected it keeps to the indexes that board installs from. In the editor
it is **mpftp: Search Packages…**, and also in the ⋯ menu of the File Transfer
view and the browser interface. An OmniPython package installs with
`mpftp omip`. A PyDevices package's install line carries
`--index https://PyDevices.github.io/mip`, which `mip` needs to find it. A
package on both indexes is listed from each, with its own version.

Without `--text` the answer is JSON: each result has `name`, `source`,
`version`, `description` and an `install` with the `command` to run. Pass
Expand Down
8 changes: 8 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
"command": "mpftp.searchPackages",
"title": "mpftp: Search Packages…"
},
{
"command": "mpftp.omipInstall",
"title": "mpftp: omip Install Package"
},
{
"command": "mpftp.mipInstall",
"title": "mpftp: mip Install Package"
Expand Down Expand Up @@ -199,6 +203,10 @@
"command": "mpftp.disconnect",
"when": "mpftp.connected"
},
{
"command": "mpftp.omipInstall",
"when": "false"
},
{
"command": "mpftp.mipInstall",
"when": "false"
Expand Down
45 changes: 42 additions & 3 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,25 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const interpreter = bridge.interpreter || "micropython";
if (interpreter === "circuitpython") {
await vscode.commands.executeCommand("mpftp.circupInstall");
} else {
await vscode.commands.executeCommand("mpftp.mipInstall");
return;
}
const how = await vscode.window.showQuickPick(
[
{
label: "omip",
detail: "An OmniPython package and what it needs from micropython-lib",
command: "mpftp.omipInstall",
},
{
label: "mip",
detail: "A micropython-lib name, or github:org/repo",
command: "mpftp.mipInstall",
},
],
{ placeHolder: "Install with" }
);
if (how) {
await vscode.commands.executeCommand(how.command);
}
}),
vscode.commands.registerCommand("mpftp.searchPackages", async () => {
Expand All @@ -457,7 +474,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
prompt:
interpreter === "circuitpython"
? "Search the Adafruit and community CircuitPython bundles"
: "Search micropython-lib and the PyDevices index",
: "Search the OmniPython index, micropython-lib and the PyDevices index",
placeHolder: "words from the package's name or description",
});
if (!query || !query.trim()) {
Expand Down Expand Up @@ -529,6 +546,28 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
log.show(true);
void vscode.window.showInformationMessage(`mip installed ${pkg}`);
}),
vscode.commands.registerCommand("mpftp.omipInstall", async () => {
if (!(await ensureConnected())) {
return;
}
const pkg = await vscode.window.showInputBox({
prompt: "Package to install via omip (host downloads, writes to board)",
placeHolder: "OmniPython or micropython-lib name, e.g. palettes",
});
if (!pkg) {
return;
}
const res = await bridge.request<{ output: string; target?: string }>("omip_install", {
packages: [pkg],
mpy: true,
});
log.appendLine(res.output || "");
if (res.target) {
log.appendLine(`target: ${res.target}`);
}
log.show(true);
void vscode.window.showInformationMessage(`omip installed ${pkg}`);
}),
vscode.commands.registerCommand("mpftp.circupInstall", async () => {
if (!(await ensureConnected())) {
return;
Expand Down
10 changes: 5 additions & 5 deletions integrations/claude-code-plugin/skills/board-tools/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ including one you just started. Three commands exist because of this:
File commands write through the mounted CIRCUITPY drive when it's there, so
they don't interrupt the running program; serial is the fallback. Compiling
to `.mpy` on upload is MicroPython only. Libraries install with `mpftp
circup`; MicroPython packages with `mpftp mip`. When you don't know a
package's name, `mpftp search WORDS` looks through micropython-lib, the
PyDevices index and the CircuitPython bundles and gives each hit's install
line (a PyDevices package needs `--index https://PyDevices.github.io/mip`,
and the line carries it).
circup`; MicroPython packages with `mpftp mip`, and OmniPython packages with
`mpftp omip`. When you don't know a package's name, `mpftp search WORDS` looks
through micropython-lib, the OmniPython and PyDevices indexes and the
CircuitPython bundles and gives each hit's install line (a PyDevices package
needs `--index https://PyDevices.github.io/mip`, and the line carries it).

## Firmware

Expand Down
Loading
Loading