Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 5 additions & 7 deletions circup/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ def install_module(
# Create the library directory first.
self.create_directory(device_path, library_path)
if local_path is None:
if pyext:
# Fallback to the source version (py) if the bundle doesn't have
# a compiled version (mpy)
if pyext or bundle.platform is None:
# Use Python source for module.
self.install_module_py(metadata)
else:
Expand Down Expand Up @@ -648,9 +650,7 @@ def install_module_mpy(self, bundle, metadata):
if not module_name:
# Must be a directory based module.
module_name = os.path.basename(os.path.dirname(metadata["path"]))
major_version = self.get_circuitpython_version()[0].split(".")[0]
bundle_platform = "{}mpy".format(major_version)
bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
bundle_path = os.path.join(bundle.lib_dir(), module_name)
if os.path.isdir(bundle_path):

self.install_dir_http(bundle_path)
Expand Down Expand Up @@ -920,9 +920,7 @@ def install_module_mpy(self, bundle, metadata):
# Must be a directory based module.
module_name = os.path.basename(os.path.dirname(metadata["path"]))

major_version = self.get_circuitpython_version()[0].split(".")[0]
bundle_platform = "{}mpy".format(major_version)
bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
bundle_path = os.path.join(bundle.lib_dir(), module_name)
if os.path.isdir(bundle_path):
target_path = os.path.join(self.library_path, module_name)
# Copy the directory.
Expand Down
72 changes: 45 additions & 27 deletions circup/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from circup.logging import logger


class Bundle:
class Bundle: # pylint: disable=too-many-instance-attributes
"""
All the links and file names for a bundle
"""
Expand Down Expand Up @@ -50,29 +50,40 @@ def __init__(self, repo):
self._latest = None
self.pinned_tag = None
self._available = []
#
self.platform = None

def lib_dir(self, platform):
def lib_dir(self, source=False):
"""
This bundle's lib directory for the platform.
This bundle's lib directory for the bundle's source or compiled version.

:param str platform: The platform identifier (py/6mpy/...).
:return: The path to the lib directory for the platform.
:param bool source: Whether to return the path to the source lib
directory or to :py:attr:`self.platform`'s lib directory. If `source` is
`False` but :py:attr:`self.platform` is None, the source lib directory
will be returned instead.
:return: The path to the lib directory.
"""
tag = self.current_tag
platform = "py" if source or not self.platform else self.platform
return os.path.join(
self.dir.format(platform=platform),
self.basename.format(platform=PLATFORMS[platform], tag=tag),
"lib",
)

def examples_dir(self, platform):
def examples_dir(self, source=False):
"""
This bundle's examples directory for the platform.
This bundle's examples directory for the bundle's source or compiled
version.

:param str platform: The platform identifier (py/6mpy/...).
:return: The path to the examples directory for the platform.
:param bool source: Whether to return the path to the source examples
directory or to :py:attr:`self.platform`'s examples directory. If
`source` is `False` but :py:attr:`self.platform` is None, the source
examples directory will be returned instead.
:return: The path to the examples directory.
"""
tag = self.current_tag
platform = "py" if source or not self.platform else self.platform
return os.path.join(
self.dir.format(platform=platform),
self.basename.format(platform=PLATFORMS[platform], tag=tag),
Expand Down Expand Up @@ -104,18 +115,25 @@ def requirements_for(self, library_name, toml_file=False):
def current_tag(self):
"""
The current tag for the project. If the tag hasn't been explicitly set
this will be the pinned tag, if one is set. If there is no pinned tag,
this will be the latest available tag that is locally available.
this will be the pinned tag, if one is set and it is available. If there
is no pinned tag, this will be the latest available tag that is locally
available.

:return: The current tag value for the project.
"""
if self._current is None:
self._current = self.pinned_tag or (
# This represents the latest version locally available
self._available[-1]
if len(self._available) > 0
else None
)
if self.pinned_tag:
self._current = (
self.pinned_tag if self.pinned_tag in self._available else None
)
else:
self._current = (
# This represents the latest version locally available
self._available[-1]
if len(self._available) > 0
else None
)

return self._current

@current_tag.setter
Expand Down Expand Up @@ -161,6 +179,7 @@ def available_tags(self, tags):
"""
if isinstance(tags, str):
tags = [tags]
# TODO: Need to pass int to sorted key...otherwise this might not sort them how it should
self._available = sorted(tags)
Comment on lines +182 to 183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still the changed mention here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I address this in #263, so depending which (if either) gets merged first I will have to do some updating and can remove this after that


def add_tag(self, tag: str) -> None:
Expand All @@ -187,22 +206,21 @@ def add_tag(self, tag: str) -> None:

def validate(self):
"""
Test the existence of the expected URLs (not their content)
Test the existence of the expected URL (not the content)
"""
tag = self.latest_tag
if not tag or tag == "releases":
if "--verbose" in sys.argv:
click.secho(f' Invalid tag "{tag}"', fg="red")
return False
for platform in PLATFORMS.values():
url = self.url_format.format(platform=platform, tag=tag)
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
# pylint: disable=no-member
if r.status_code != requests.codes.ok:
if "--verbose" in sys.argv:
click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
return False
# pylint: enable=no-member
url = self.url_format.format(platform="py", tag=tag)
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
# pylint: disable=no-member
if r.status_code != requests.codes.ok:
if "--verbose" in sys.argv:
click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
return False
# pylint: enable=no-member
return True

def __repr__(self):
Expand Down
127 changes: 79 additions & 48 deletions circup/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,35 +149,67 @@ def ensure_bundle_tag(bundle, tag):
logger.warning("Bundle version requested is 'None'.")
return False

do_update = False
do_update_source = False
do_update_compiled = False
if tag in bundle.available_tags:
for platform in PLATFORMS:
# missing directories (new platform added on an existing install
# or side effect of pytest or network errors)
do_update = do_update or not os.path.isdir(bundle.lib_dir(platform))
# missing directories (new platform added on an existing install
# or side effect of pytest or network errors)
# Check for the source
do_update_source = not os.path.isdir(bundle.lib_dir(source=True))
do_update_compiled = bundle.platform is not None and not os.path.isdir(
bundle.lib_dir(source=False)
)
else:
do_update = True
do_update_source = True
do_update_compiled = bundle.platform is not None

if not (do_update_source or do_update_compiled):
logger.info("Current bundle version available (%s).", tag)
return True

if do_update:
if Bundle.offline:
if Bundle.offline:
if do_update_source: # pylint: disable=no-else-return
logger.info(
"Bundle version not available but skipping update in offline mode."
)
return False
else:
logger.info(
"Bundle platform not available. Falling back to source (.py) files in offline mode."
)
bundle.platform = None
return True

logger.info("New version available (%s).", tag)
logger.info("New version available (%s).", tag)
if do_update_source:
try:
get_bundle(bundle, tag)
tags_data_save_tags(bundle.key, bundle.available_tags)
get_bundle(bundle, tag, "py")
except requests.exceptions.HTTPError as ex:
click.secho(
f"There was a problem downloading the {bundle.key} bundle.", fg="red"
f"There was a problem downloading the 'py' platform for the '{bundle.key}' bundle.",
fg="red",
)
logger.exception(ex)
return False
else:
logger.info("Current bundle version available (%s).", tag)
return True
return False # Bundle isn't available
bundle.add_tag(tag)
tags_data_save_tags(bundle.key, bundle.available_tags)

if do_update_compiled:
try:
get_bundle(bundle, tag, bundle.platform)
except requests.exceptions.HTTPError as ex:
click.secho(
(
f"There was a problem downloading the '{bundle.platform}' platform for the "
f"'{bundle.key}' bundle.\nFalling back to source (.py) files."
),
fg="red",
)
logger.exception(ex)
bundle.platform = None # Compiled isn't available, source is good
bundle.current_tag = tag

return True # bundle is available


def ensure_latest_bundle(bundle):
Expand Down Expand Up @@ -364,40 +396,38 @@ def find_modules(backend, bundles_list):
# pylint: enable=broad-except,too-many-locals


def get_bundle(bundle, tag):
def get_bundle(bundle, tag, platform):
Copy link
Contributor

@FoamyGuy FoamyGuy Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for this and the other functions where platform or platform_version argument has been added can you please add them to the docstrings as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, think I fixed this, let me know if I missed any others!

"""
Downloads and extracts the version of the bundle with the referenced tag.
The resulting zip file is saved on the local filesystem.

:param Bundle bundle: the target Bundle object.
:param str tag: The GIT tag to use to download the bundle.
"""
click.echo(f"Downloading bundles for {bundle.key} ({tag}).")
for platform, github_string in PLATFORMS.items():
# Report the platform: "8.x-mpy", etc.
click.echo(f"{github_string}:")
url = bundle.url_format.format(platform=github_string, tag=tag)
logger.info("Downloading bundle: %s", url)
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
# pylint: disable=no-member
if r.status_code != requests.codes.ok:
logger.warning("Unable to connect to %s", url)
r.raise_for_status()
# pylint: enable=no-member
total_size = int(r.headers.get("Content-Length"))
temp_zip = bundle.zip.format(platform=platform)
with click.progressbar(
r.iter_content(1024), label="Extracting:", length=total_size
) as pbar, open(temp_zip, "wb") as zip_fp:
for chunk in pbar:
zip_fp.write(chunk)
pbar.update(len(chunk))
logger.info("Saved to %s", temp_zip)
temp_dir = bundle.dir.format(platform=platform)
with zipfile.ZipFile(temp_zip, "r") as zfile:
zfile.extractall(temp_dir)
bundle.add_tag(tag)
bundle.current_tag = tag
click.echo(f"Downloading '{platform}' bundle for {bundle.key} ({tag}).")
github_string = PLATFORMS[platform]
# Report the platform: "8.x-mpy", etc.
click.echo(f"{github_string}:")
url = bundle.url_format.format(platform=github_string, tag=tag)
logger.info("Downloading bundle: %s", url)
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
# pylint: disable=no-member
if r.status_code != requests.codes.ok:
logger.warning("Unable to connect to %s", url)
r.raise_for_status()
# pylint: enable=no-member
total_size = int(r.headers.get("Content-Length"))
temp_zip = bundle.zip.format(platform=platform)
with click.progressbar(
r.iter_content(1024), label="Extracting:", length=total_size
) as pbar, open(temp_zip, "wb") as zip_fp:
for chunk in pbar:
zip_fp.write(chunk)
pbar.update(len(chunk))
logger.info("Saved to %s", temp_zip)
temp_dir = bundle.dir.format(platform=platform)
with zipfile.ZipFile(temp_zip, "r") as zfile:
zfile.extractall(temp_dir)
click.echo("\nOK\n")


Expand All @@ -417,9 +447,9 @@ def get_bundle_examples(bundles_list, avoid_download=False):

try:
for bundle in bundles_list:
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
ensure_bundle(bundle)
path = bundle.examples_dir("py")
path = bundle.examples_dir(source=True)
meta_saved = os.path.join(path, "../bundle_examples.json")
if os.path.exists(meta_saved):
with open(meta_saved, "r", encoding="utf-8") as f:
Expand Down Expand Up @@ -465,9 +495,9 @@ def get_bundle_versions(bundles_list, avoid_download=False):
"""
all_the_modules = dict()
for bundle in bundles_list:
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
ensure_bundle(bundle)
path = bundle.lib_dir("py")
path = bundle.lib_dir(source=True)
path_modules = _get_modules_file(path, logger)
for name, module in path_modules.items():
module["bundle"] = bundle
Expand Down Expand Up @@ -514,7 +544,7 @@ def get_bundles_local_dict():
return dict()


def get_bundles_list(bundle_tags):
def get_bundles_list(bundle_tags, platform_version=None):
"""
Retrieve the list of bundles from the config dictionary.

Expand All @@ -534,6 +564,7 @@ def get_bundles_list(bundle_tags):

bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
for bundle in bundles_list:
bundle.platform = platform_version
bundle.available_tags = tags.get(bundle.key, [])
if pinned_tags is not None:
bundle.pinned_tag = pinned_tags.get(bundle.key)
Expand Down
Loading