diff --git a/circup/bundle.py b/circup/bundle.py new file mode 100644 index 0000000..e69de29 diff --git a/circup/class_definitions.py b/circup/class_definitions.py index 24db1b1..4a91f3d 100644 --- a/circup/class_definitions.py +++ b/circup/class_definitions.py @@ -150,196 +150,3 @@ class Bundle: ) -class Module: - """ - Represents a CircuitPython module. - """ - - # pylint: disable=too-many-arguments - - def __init__( - self, - name, - backend, - repo, - device_version, - bundle_version, - mpy, - bundle, - compatibility, - ): - """ - The ``self.file`` and ``self.name`` attributes are constructed from - the ``path`` value. If the path is to a directory based module, the - resulting self.file value will be None, and the name will be the - basename of the directory path. - - :param str name: The file name of the module. - :param Backend backend: The backend that the module is on. - :param str repo: The URL of the Git repository for this module. - :param str device_version: The semver value for the version on device. - :param str bundle_version: The semver value for the version in bundle. - :param bool mpy: Flag to indicate if the module is byte-code compiled. - :param Bundle bundle: Bundle object where the module is located. - :param (str,str) compatibility: Min and max versions of CP compatible with the mpy. - """ - self.name = name - self.backend = backend - self.path = ( - urljoin(backend.library_path, name, allow_fragments=False) - if isinstance(backend, WebBackend) - else os.path.join(backend.library_path, name) - ) - - url = urlparse(self.path, allow_fragments=False) - - if ( - url.path.endswith("/") - if isinstance(backend, WebBackend) - else self.path.endswith(os.sep) - ): - self.file = None - self.name = self.path.split( - "/" if isinstance(backend, WebBackend) else os.sep - )[-2] - else: - self.file = os.path.basename(url.path) - self.name = ( - os.path.basename(url.path).replace(".py", "").replace(".mpy", "") - ) - - self.repo = repo - self.device_version = device_version - self.bundle_version = bundle_version - self.mpy = mpy - self.min_version = compatibility[0] - self.max_version = compatibility[1] - # Figure out the bundle path. - self.bundle_path = None - if self.mpy: - # Byte compiled, now check CircuitPython version. - major_version = CPY_VERSION.split(".")[0] - bundle_platform = "{}mpy".format(major_version) - else: - # Regular Python - bundle_platform = "py" - # module path in the bundle - search_path = bundle.lib_dir(bundle_platform) - if self.file: - self.bundle_path = os.path.join(search_path, self.file) - else: - self.bundle_path = os.path.join(search_path, self.name) - logger.info(self) - - # pylint: enable=too-many-arguments - - @property - def outofdate(self): - """ - Returns a boolean to indicate if this module is out of date. - Treat mismatched MPY versions as out of date. - - :return: Truthy indication if the module is out of date. - """ - if self.mpy_mismatch: - return True - if self.device_version and self.bundle_version: - try: - return VersionInfo.parse(self.device_version) < VersionInfo.parse( - self.bundle_version - ) - except ValueError as ex: - logger.warning("Module '%s' has incorrect semver value.", self.name) - logger.warning(ex) - return True # Assume out of date to try to update. - - @property - def bad_format(self): - """A boolean indicating that the mpy file format could not be identified""" - return self.mpy and self.device_version == BAD_FILE_FORMAT - - @property - def mpy_mismatch(self): - """ - Returns a boolean to indicate if this module's MPY version is compatible - with the board's current version of Circuitpython. A min or max version - that evals to False means no limit. - - :return: Boolean indicating if the MPY versions don't match. - """ - if not self.mpy: - return False - try: - cpv = VersionInfo.parse(CPY_VERSION) - except ValueError as ex: - logger.warning("CircuitPython has incorrect semver value.") - logger.warning(ex) - try: - if self.min_version and cpv < VersionInfo.parse(self.min_version): - return True # CP version too old - if self.max_version and cpv >= VersionInfo.parse(self.max_version): - return True # MPY version too old - except (TypeError, ValueError) as ex: - logger.warning( - "Module '%s' has incorrect MPY compatibility information.", self.name - ) - logger.warning(ex) - return False - - @property - def major_update(self): - """ - Returns a boolean to indicate if this is a major version update. - - :return: Boolean indicating if this is a major version upgrade - """ - try: - if ( - VersionInfo.parse(self.device_version).major - == VersionInfo.parse(self.bundle_version).major - ): - return False - except (TypeError, ValueError) as ex: - logger.warning("Module '%s' has incorrect semver value.", self.name) - logger.warning(ex) - return True # Assume Major Version udpate. - - @property - def row(self): - """ - Returns a tuple of items to display in a table row to show the module's - name, local version and remote version, and reason to update. - - :return: A tuple containing the module's name, version on the connected - device, version in the latest bundle and reason to update. - """ - loc = self.device_version if self.device_version else "unknown" - rem = self.bundle_version if self.bundle_version else "unknown" - if self.mpy_mismatch: - update_reason = "MPY Format" - elif self.major_update: - update_reason = "Major Version" - else: - update_reason = "Minor Version" - return (self.name, loc, rem, update_reason) - - def __repr__(self): - """ - Helps with log files. - - :return: A repr of a dictionary containing the module's metadata. - """ - return repr( - { - "path": self.path, - "file": self.file, - "name": self.name, - "repo": self.repo, - "device_version": self.device_version, - "bundle_version": self.bundle_version, - "bundle_path": self.bundle_path, - "mpy": self.mpy, - "min_version": self.min_version, - "max_version": self.max_version, - } - ) diff --git a/circup/module.py b/circup/module.py new file mode 100644 index 0000000..a011c6d --- /dev/null +++ b/circup/module.py @@ -0,0 +1,202 @@ +import os +from urllib.parse import urljoin, urlparse +from semver import VersionInfo + +from circup.shared import BAD_FILE_FORMAT, \ +from circup.backends import WebBackend +from circup.logging import logger + +class Module: + """ + Represents a CircuitPython module. + """ + + # pylint: disable=too-many-arguments + + def __init__( + self, + name, + backend, + repo, + device_version, + bundle_version, + mpy, + bundle, + compatibility, + ): + """ + The ``self.file`` and ``self.name`` attributes are constructed from + the ``path`` value. If the path is to a directory based module, the + resulting self.file value will be None, and the name will be the + basename of the directory path. + + :param str name: The file name of the module. + :param Backend backend: The backend that the module is on. + :param str repo: The URL of the Git repository for this module. + :param str device_version: The semver value for the version on device. + :param str bundle_version: The semver value for the version in bundle. + :param bool mpy: Flag to indicate if the module is byte-code compiled. + :param Bundle bundle: Bundle object where the module is located. + :param (str,str) compatibility: Min and max versions of CP compatible with the mpy. + """ + self.name = name + self.backend = backend + self.path = ( + urljoin(backend.library_path, name, allow_fragments=False) + if isinstance(backend, WebBackend) + else os.path.join(backend.library_path, name) + ) + + url = urlparse(self.path, allow_fragments=False) + + if ( + url.path.endswith("/") + if isinstance(backend, WebBackend) + else self.path.endswith(os.sep) + ): + self.file = None + self.name = self.path.split( + "/" if isinstance(backend, WebBackend) else os.sep + )[-2] + else: + self.file = os.path.basename(url.path) + self.name = ( + os.path.basename(url.path).replace(".py", "").replace(".mpy", "") + ) + + self.repo = repo + self.device_version = device_version + self.bundle_version = bundle_version + self.mpy = mpy + self.min_version = compatibility[0] + self.max_version = compatibility[1] + # Figure out the bundle path. + self.bundle_path = None + if self.mpy: + # Byte compiled, now check CircuitPython version. + + major_version = CPY_VERSION.split(".")[0] + bundle_platform = "{}mpy".format(major_version) + else: + # Regular Python + bundle_platform = "py" + # module path in the bundle + search_path = bundle.lib_dir(bundle_platform) + if self.file: + self.bundle_path = os.path.join(search_path, self.file) + else: + self.bundle_path = os.path.join(search_path, self.name) + logger.info(self) + + # pylint: enable=too-many-arguments + + @property + def outofdate(self): + """ + Returns a boolean to indicate if this module is out of date. + Treat mismatched MPY versions as out of date. + + :return: Truthy indication if the module is out of date. + """ + if self.mpy_mismatch: + return True + if self.device_version and self.bundle_version: + try: + return VersionInfo.parse(self.device_version) < VersionInfo.parse( + self.bundle_version + ) + except ValueError as ex: + logger.warning("Module '%s' has incorrect semver value.", self.name) + logger.warning(ex) + return True # Assume out of date to try to update. + + @property + def bad_format(self): + """A boolean indicating that the mpy file format could not be identified""" + return self.mpy and self.device_version == BAD_FILE_FORMAT + + @property + def mpy_mismatch(self): + """ + Returns a boolean to indicate if this module's MPY version is compatible + with the board's current version of Circuitpython. A min or max version + that evals to False means no limit. + + :return: Boolean indicating if the MPY versions don't match. + """ + if not self.mpy: + return False + try: + cpv = VersionInfo.parse(CPY_VERSION) + except ValueError as ex: + logger.warning("CircuitPython has incorrect semver value.") + logger.warning(ex) + try: + if self.min_version and cpv < VersionInfo.parse(self.min_version): + return True # CP version too old + if self.max_version and cpv >= VersionInfo.parse(self.max_version): + return True # MPY version too old + except (TypeError, ValueError) as ex: + logger.warning( + "Module '%s' has incorrect MPY compatibility information.", self.name + ) + logger.warning(ex) + return False + + @property + def major_update(self): + """ + Returns a boolean to indicate if this is a major version update. + + :return: Boolean indicating if this is a major version upgrade + """ + try: + if ( + VersionInfo.parse(self.device_version).major + == VersionInfo.parse(self.bundle_version).major + ): + return False + except (TypeError, ValueError) as ex: + logger.warning("Module '%s' has incorrect semver value.", self.name) + logger.warning(ex) + return True # Assume Major Version udpate. + + @property + def row(self): + """ + Returns a tuple of items to display in a table row to show the module's + name, local version and remote version, and reason to update. + + :return: A tuple containing the module's name, version on the connected + device, version in the latest bundle and reason to update. + """ + loc = self.device_version if self.device_version else "unknown" + rem = self.bundle_version if self.bundle_version else "unknown" + if self.mpy_mismatch: + update_reason = "MPY Format" + elif self.major_update: + update_reason = "Major Version" + else: + update_reason = "Minor Version" + return (self.name, loc, rem, update_reason) + + def __repr__(self): + """ + Helps with log files. + + :return: A repr of a dictionary containing the module's metadata. + """ + return repr( + { + "path": self.path, + "file": self.file, + "name": self.name, + "repo": self.repo, + "device_version": self.device_version, + "bundle_version": self.bundle_version, + "bundle_path": self.bundle_path, + "mpy": self.mpy, + "min_version": self.min_version, + "max_version": self.max_version, + } + )