Merge remote-tracking branch 'origin/doxygen_to_restructuredtext_comments' into doxygen_to_restructuredtext_comments

This commit is contained in:
Jelle Spijker 2020-05-08 19:00:47 +02:00
commit a503149452
No known key found for this signature in database
GPG Key ID: 6662DC033BE6B99A
30 changed files with 540 additions and 397 deletions

View File

@ -13,23 +13,30 @@ from cura.ReaderWriters.ProfileReader import ProfileReader
import zipfile
## A plugin that reads profile data from Cura profile files.
#
# It reads a profile from a .curaprofile file, and returns it as a profile
# instance.
class CuraProfileReader(ProfileReader):
## Initialises the cura profile reader.
# This does nothing since the only other function is basically stateless.
"""A plugin that reads profile data from Cura profile files.
It reads a profile from a .curaprofile file, and returns it as a profile
instance.
"""
def __init__(self) -> None:
"""Initialises the cura profile reader.
This does nothing since the only other function is basically stateless.
"""
super().__init__()
## Reads a cura profile from a file and returns it.
#
# \param file_name The file to read the cura profile from.
# \return The cura profiles that were in the file, if any. If the file
# could not be read or didn't contain a valid profile, ``None`` is
# returned.
def read(self, file_name: str) -> List[Optional[InstanceContainer]]:
"""Reads a cura profile from a file and returns it.
:param file_name: The file to read the cura profile from.
:return: The cura profiles that were in the file, if any. If the file
could not be read or didn't contain a valid profile, ``None`` is
returned.
"""
try:
with zipfile.ZipFile(file_name, "r") as archive:
results = [] # type: List[Optional[InstanceContainer]]
@ -50,13 +57,14 @@ class CuraProfileReader(ProfileReader):
serialized_bytes = fhandle.read()
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized_bytes, file_name)]
## Convert a profile from an old Cura to this Cura if needed.
#
# \param serialized The profile data to convert in the serialized on-disk
# format.
# \param profile_id The name of the profile.
# \return List of serialized profile strings and matching profile names.
def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]:
"""Convert a profile from an old Cura to this Cura if needed.
:param serialized: The profile data to convert in the serialized on-disk format.
:param profile_id: The name of the profile.
:return: List of serialized profile strings and matching profile names.
"""
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@ -75,12 +83,14 @@ class CuraProfileReader(ProfileReader):
else:
return [(serialized, profile_id)]
## Load a profile from a serialized string.
#
# \param serialized The profile data to read.
# \param profile_id The name of the profile.
# \return The profile that was stored in the string.
def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]:
"""Load a profile from a serialized string.
:param serialized: The profile data to read.
:param profile_id: The name of the profile.
:return: The profile that was stored in the string.
"""
# Create an empty profile.
profile = InstanceContainer(profile_id)
profile.setMetaDataEntry("type", "quality_changes")
@ -102,13 +112,15 @@ class CuraProfileReader(ProfileReader):
profile.setMetaDataEntry("definition", active_quality_definition)
return profile
## Upgrade a serialized profile to the current profile format.
#
# \param serialized The profile data to convert.
# \param profile_id The name of the profile.
# \param source_version The profile version of 'serialized'.
# \return List of serialized profile strings and matching profile names.
def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]:
"""Upgrade a serialized profile to the current profile format.
:param serialized: The profile data to convert.
:param profile_id: The name of the profile.
:param source_version: The profile version of 'serialized'.
:return: List of serialized profile strings and matching profile names.
"""
source_version = main_version * 1000000 + setting_version
from UM.VersionUpgradeManager import VersionUpgradeManager

View File

@ -6,15 +6,18 @@ from UM.Logger import Logger
from cura.ReaderWriters.ProfileWriter import ProfileWriter
import zipfile
## Writes profiles to Cura's own profile format with config files.
class CuraProfileWriter(ProfileWriter):
## Writes a profile to the specified file path.
#
# \param path \type{string} The file to output to.
# \param profiles \type{Profile} \type{List} The profile(s) to write to that file.
# \return \code True \endcode if the writing was successful, or \code
# False \endcode if it wasn't.
"""Writes profiles to Cura's own profile format with config files."""
def write(self, path, profiles):
"""Writes a profile to the specified file path.
:param path: :type{string} The file to output to.
:param profiles: :type{Profile} :type{List} The profile(s) to write to that file.
:return: True if the writing was successful, or
False if it wasn't.
"""
if type(profiles) != list:
profiles = [profiles]

View File

@ -18,10 +18,12 @@ from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
i18n_catalog = i18nCatalog("cura")
## This Extension checks for new versions of the firmware based on the latest checked version number.
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications.
class FirmwareUpdateChecker(Extension):
"""This Extension checks for new versions of the firmware based on the latest checked version number.
The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
to change it to work for other applications.
"""
def __init__(self) -> None:
super().__init__()
@ -35,8 +37,9 @@ class FirmwareUpdateChecker(Extension):
self._check_job = None
self._checked_printer_names = set() # type: Set[str]
## Callback for the message that is spawned when there is a new version.
def _onActionTriggered(self, message, action):
"""Callback for the message that is spawned when there is a new version."""
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
machine_id = message.getMachineId()
download_url = message.getDownloadUrl()
@ -57,13 +60,15 @@ class FirmwareUpdateChecker(Extension):
def _onJobFinished(self, *args, **kwargs):
self._check_job = None
## Connect with software.ultimaker.com, load latest.version and check version info.
# If the version info is different from the current version, spawn a message to
# allow the user to download it.
#
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
# This is used when checking for a new firmware version at startup.
def checkFirmwareVersion(self, container = None, silent = False):
"""Connect with software.ultimaker.com, load latest.version and check version info.
If the version info is different from the current version, spawn a message to
allow the user to download it.
:param silent: type(boolean) Suppresses messages other than "new version found" messages.
This is used when checking for a new firmware version at startup.
"""
container_name = container.definition.getName()
if container_name in self._checked_printer_names:
return

View File

@ -21,8 +21,9 @@ from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
## This job checks if there is an update available on the provided URL.
class FirmwareUpdateCheckerJob(Job):
"""This job checks if there is an update available on the provided URL."""
STRING_ZERO_VERSION = "0.0.0"
STRING_EPSILON_VERSION = "0.0.1"
ZERO_VERSION = Version(STRING_ZERO_VERSION)

View File

@ -19,8 +19,10 @@ if MYPY:
catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class FirmwareUpdaterMachineAction(MachineAction):
"""Upgrade the firmware of a machine by USB with this action."""
def __init__(self) -> None:
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
self._qml_url = "FirmwareUpdaterMachineAction.qml"

View File

@ -7,10 +7,13 @@ from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementin
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database.
from UM.PluginRegistry import PluginRegistry
## A file reader that reads gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader):
"""A file reader that reads gzipped g-code.
If you're zipping g-code, you might as well use gzip!
"""
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(

View File

@ -13,26 +13,31 @@ from UM.Scene.SceneNode import SceneNode #For typing.
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## A file writer that writes gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzWriter(MeshWriter):
"""A file writer that writes gzipped g-code.
If you're zipping g-code, you might as well use gzip!
"""
def __init__(self) -> None:
super().__init__(add_to_recent_files = False)
## Writes the gzipped g-code to a stream.
#
# Note that even though the function accepts a collection of nodes, the
# entire scene is always written to the file since it is not possible to
# separate the g-code for just specific nodes.
#
# \param stream The stream to write the gzipped g-code to.
# \param nodes This is ignored.
# \param mode Additional information on what type of stream to use. This
# must always be binary mode.
# \return Whether the write was successful.
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool:
"""Writes the gzipped g-code to a stream.
Note that even though the function accepts a collection of nodes, the
entire scene is always written to the file since it is not possible to
separate the g-code for just specific nodes.
:param stream: The stream to write the gzipped g-code to.
:param nodes: This is ignored.
:param mode: Additional information on what type of stream to use. This
must always be binary mode.
:return: Whether the write was successful.
"""
if mode != MeshWriter.OutputMode.BinaryMode:
Logger.log("e", "GCodeGzWriter does not support text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode."))

View File

@ -12,40 +12,48 @@ catalog = i18nCatalog("cura")
from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException
## A class that reads profile data from g-code files.
#
# It reads the profile data from g-code files and stores it in a new profile.
# This class currently does not process the rest of the g-code in any way.
class GCodeProfileReader(ProfileReader):
## The file format version of the serialized g-code.
#
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 3
"""A class that reads profile data from g-code files.
It reads the profile data from g-code files and stores it in a new profile.
This class currently does not process the rest of the g-code in any way.
"""
version = 3
"""The file format version of the serialized g-code.
It can only read settings with the same version as the version it was
written with. If the file format is changed in a way that breaks reverse
compatibility, increment this version number!
"""
## Dictionary that defines how characters are escaped when embedded in
# g-code.
#
# Note that the keys of this dictionary are regex strings. The values are
# not.
escape_characters = {
re.escape("\\\\"): "\\", #The escape character.
re.escape("\\n"): "\n", #Newlines. They break off the comment.
re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors.
}
"""Dictionary that defines how characters are escaped when embedded in
g-code.
Note that the keys of this dictionary are regex strings. The values are
not.
"""
## Initialises the g-code reader as a profile reader.
def __init__(self):
"""Initialises the g-code reader as a profile reader."""
super().__init__()
## Reads a g-code file, loading the profile from it.
#
# \param file_name The name of the file to read the profile from.
# \return The profile that was in the specified file, if any. If the
# specified file was no g-code or contained no parsable profile, \code
# None \endcode is returned.
def read(self, file_name):
"""Reads a g-code file, loading the profile from it.
:param file_name: The name of the file to read the profile from.
:return: The profile that was in the specified file, if any. If the
specified file was no g-code or contained no parsable profile,
None is returned.
"""
if file_name.split(".")[-1] != "gcode":
return None
@ -94,22 +102,28 @@ class GCodeProfileReader(ProfileReader):
profiles.append(readQualityProfileFromString(profile_string))
return profiles
## Unescape a string which has been escaped for use in a gcode comment.
#
# \param string The string to unescape.
# \return \type{str} The unscaped string.
def unescapeGcodeComment(string):
def unescapeGcodeComment(string: str) -> str:
"""Unescape a string which has been escaped for use in a gcode comment.
:param string: The string to unescape.
:return: The unescaped string.
"""
# Un-escape the serialized profile.
pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
# Perform the replacement with a regular expression.
return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
## Read in a profile from a serialized string.
#
# \param profile_string The profile data in serialized form.
# \return \type{Profile} the resulting Profile object or None if it could not be read.
def readQualityProfileFromString(profile_string):
def readQualityProfileFromString(profile_string) -> InstanceContainer:
"""Read in a profile from a serialized string.
:param profile_string: The profile data in serialized form.
:return: The resulting Profile object or None if it could not be read.
"""
# Create an empty profile - the id and name will be changed by the ContainerRegistry
profile = InstanceContainer("")
try:

View File

@ -28,9 +28,8 @@ PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optiona
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
## This parser is intended to interpret the common firmware codes among all the
# different flavors
class FlavorParser:
"""This parser is intended to interpret the common firmware codes among all the different flavors"""
def __init__(self) -> None:
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
@ -212,8 +211,9 @@ class FlavorParser:
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Home the head."""
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
@ -221,21 +221,26 @@ class FlavorParser:
position.f,
position.e)
## Set the absolute positioning
def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Set the absolute positioning"""
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Set the relative positioning"""
self._is_absolute_positioning = False
self._is_absolute_extrusion = False
return position
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Reset the current position to the values specified.
For example: G92 X10 will set the X to 10 without any physical motion.
"""
if params.e is not None:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
@ -291,8 +296,9 @@ class FlavorParser:
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self) -> Dict[int, List[float]]:
"""For showing correct x, y offsets for each extruder"""
result = {}
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [

View File

@ -3,8 +3,10 @@
from . import FlavorParser
## This parser is intended to interpret the RepRap Firmware g-code flavor.
class RepRapFlavorParser(FlavorParser.FlavorParser):
"""This parser is intended to interpret the RepRap Firmware g-code flavor."""
def __init__(self):
super().__init__()
@ -17,16 +19,20 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
# Set relative extrusion mode
self._is_absolute_extrusion = False
## Set the absolute positioning
# RepRapFlavor code G90 sets position of X, Y, Z to absolute
# For absolute E, M82 is used
def _gCode90(self, position, params, path):
"""Set the absolute positioning
RepRapFlavor code G90 sets position of X, Y, Z to absolute
For absolute E, M82 is used
"""
self._is_absolute_positioning = True
return position
## Set the relative positioning
# RepRapFlavor code G91 sets position of X, Y, Z to relative
# For relative E, M83 is used
def _gCode91(self, position, params, path):
"""Set the relative positioning
RepRapFlavor code G91 sets position of X, Y, Z to relative
For relative E, M83 is used
"""
self._is_absolute_positioning = False
return position

View File

@ -14,34 +14,40 @@ from cura.Machines.ContainerTree import ContainerTree
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Writes g-code to a file.
#
# While this poses as a mesh writer, what this really does is take the g-code
# in the entire scene and write it to an output device. Since the g-code of a
# single mesh isn't separable from the rest what with rafts and travel moves
# and all, it doesn't make sense to write just a single mesh.
#
# So this plug-in takes the g-code that is stored in the root of the scene
# node tree, adds a bit of extra information about the profiles and writes
# that to the output device.
class GCodeWriter(MeshWriter):
## The file format version of the serialised g-code.
#
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 3
## Dictionary that defines how characters are escaped when embedded in
# g-code.
#
# Note that the keys of this dictionary are regex strings. The values are
# not.
class GCodeWriter(MeshWriter):
"""Writes g-code to a file.
While this poses as a mesh writer, what this really does is take the g-code
in the entire scene and write it to an output device. Since the g-code of a
single mesh isn't separable from the rest what with rafts and travel moves
and all, it doesn't make sense to write just a single mesh.
So this plug-in takes the g-code that is stored in the root of the scene
node tree, adds a bit of extra information about the profiles and writes
that to the output device.
"""
version = 3
"""The file format version of the serialised g-code.
It can only read settings with the same version as the version it was
written with. If the file format is changed in a way that breaks reverse
compatibility, increment this version number!
"""
escape_characters = {
re.escape("\\"): "\\\\", # The escape character.
re.escape("\n"): "\\n", # Newlines. They break off the comment.
re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
}
"""Dictionary that defines how characters are escaped when embedded in
g-code.
Note that the keys of this dictionary are regex strings. The values are
not.
"""
_setting_keyword = ";SETTING_"
@ -50,17 +56,19 @@ class GCodeWriter(MeshWriter):
self._application = Application.getInstance()
## Writes the g-code for the entire scene to a stream.
#
# Note that even though the function accepts a collection of nodes, the
# entire scene is always written to the file since it is not possible to
# separate the g-code for just specific nodes.
#
# \param stream The stream to write the g-code to.
# \param nodes This is ignored.
# \param mode Additional information on how to format the g-code in the
# file. This must always be text mode.
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
"""Writes the g-code for the entire scene to a stream.
Note that even though the function accepts a collection of nodes, the
entire scene is always written to the file since it is not possible to
separate the g-code for just specific nodes.
:param stream: The stream to write the g-code to.
:param nodes: This is ignored.
:param mode: Additional information on how to format the g-code in the
file. This must always be text mode.
"""
if mode != MeshWriter.OutputMode.TextMode:
Logger.log("e", "GCodeWriter does not support non-text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode."))
@ -88,8 +96,9 @@ class GCodeWriter(MeshWriter):
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False
## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
"""Create a new container with container 2 as base and container 1 written over it."""
flat_container = InstanceContainer(instance_container2.getName())
# The metadata includes id, name and definition
@ -106,15 +115,15 @@ class GCodeWriter(MeshWriter):
return flat_container
## Serialises a container stack to prepare it for writing at the end of the
# g-code.
#
# The settings are serialised, and special characters (including newline)
# are escaped.
#
# \param settings A container stack to serialise.
# \return A serialised string of the settings.
def _serialiseSettings(self, stack):
"""Serialises a container stack to prepare it for writing at the end of the g-code.
The settings are serialised, and special characters (including newline)
are escaped.
:param stack: A container stack to serialise.
:return: A serialised string of the settings.
"""
container_registry = self._application.getContainerRegistry()
prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.

View File

@ -16,58 +16,67 @@ from UM.Settings.InstanceContainer import InstanceContainer # The new profile t
from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type to implement.
## A plugin that reads profile data from legacy Cura versions.
#
# It reads a profile from an .ini file, and performs some translations on it.
# Not all translations are correct, mind you, but it is a best effort.
class LegacyProfileReader(ProfileReader):
## Initialises the legacy profile reader.
#
# This does nothing since the only other function is basically stateless.
"""A plugin that reads profile data from legacy Cura versions.
It reads a profile from an .ini file, and performs some translations on it.
Not all translations are correct, mind you, but it is a best effort.
"""
def __init__(self):
"""Initialises the legacy profile reader.
This does nothing since the only other function is basically stateless.
"""
super().__init__()
## Prepares the default values of all legacy settings.
#
# These are loaded from the Dictionary of Doom.
#
# \param json The JSON file to load the default setting values from. This
# should not be a URL but a pre-loaded JSON handle.
# \return A dictionary of the default values of the legacy Cura version.
def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]:
"""Prepares the default values of all legacy settings.
These are loaded from the Dictionary of Doom.
:param json: The JSON file to load the default setting values from. This
should not be a URL but a pre-loaded JSON handle.
:return: A dictionary of the default values of the legacy Cura version.
"""
defaults = {}
if "defaults" in json:
for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict.
defaults[key] = json["defaults"][key]
return defaults
## Prepares the local variables that can be used in evaluation of computing
# new setting values from the old ones.
#
# This fills a dictionary with all settings from the legacy Cura version
# and their values, so that they can be used in evaluating the new setting
# values as Python code.
#
# \param config_parser The ConfigParser that finds the settings in the
# legacy profile.
# \param config_section The section in the profile where the settings
# should be found.
# \param defaults The default values for all settings in the legacy Cura.
# \return A set of local variables, one for each setting in the legacy
# profile.
def prepareLocals(self, config_parser, config_section, defaults):
"""Prepares the local variables that can be used in evaluation of computing
new setting values from the old ones.
This fills a dictionary with all settings from the legacy Cura version
and their values, so that they can be used in evaluating the new setting
values as Python code.
:param config_parser: The ConfigParser that finds the settings in the
legacy profile.
:param config_section: The section in the profile where the settings
should be found.
:param defaults: The default values for all settings in the legacy Cura.
:return: A set of local variables, one for each setting in the legacy
profile.
"""
copied_locals = defaults.copy() # Don't edit the original!
for option in config_parser.options(config_section):
copied_locals[option] = config_parser.get(config_section, option)
return copied_locals
## Reads a legacy Cura profile from a file and returns it.
#
# \param file_name The file to read the legacy Cura profile from.
# \return The legacy Cura profile that was in the file, if any. If the
# file could not be read or didn't contain a valid profile, \code None
# \endcode is returned.
def read(self, file_name):
"""Reads a legacy Cura profile from a file and returns it.
:param file_name: The file to read the legacy Cura profile from.
:return: The legacy Cura profile that was in the file, if any. If the
file could not be read or didn't contain a valid profile, None is returned.
"""
if file_name.split(".")[-1] != "ini":
return None
global_container_stack = Application.getInstance().getGlobalContainerStack()

View File

@ -13,7 +13,7 @@ import UM.PluginRegistry # To mock the plug-in registry out.
import UM.Settings.ContainerRegistry # To mock the container registry out.
import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function.
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
@pytest.fixture
@ -126,9 +126,11 @@ test_prepareLocalsNoSectionErrorData = [
)
]
## Test cases where a key error is expected.
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData)
def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults):
"""Test cases where a key error is expected."""
parser = configparser.ConfigParser()
parser.read_dict(parser_data)

View File

@ -23,9 +23,11 @@ if TYPE_CHECKING:
catalog = UM.i18n.i18nCatalog("cura")
## This action allows for certain settings that are "machine only") to be modified.
# It automatically detects machine definitions that it knows how to change and attaches itself to those.
class MachineSettingsAction(MachineAction):
"""This action allows for certain settings that are "machine only") to be modified.
It automatically detects machine definitions that it knows how to change and attaches itself to those.
"""
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml"
@ -56,9 +58,11 @@ class MachineSettingsAction(MachineAction):
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
## Triggered when the global container stack changes or when the g-code
# flavour setting is changed.
def _updateHasMaterialsInContainerTree(self) -> None:
"""Triggered when the global container stack changes or when the g-code
flavour setting is changed.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return

View File

@ -18,8 +18,8 @@ catalog = i18nCatalog("cura")
class ModelChecker(QObject, Extension):
## Signal that gets emitted when anything changed that we need to check.
onChanged = pyqtSignal()
"""Signal that gets emitted when anything changed that we need to check."""
def __init__(self):
super().__init__()
@ -47,11 +47,13 @@ class ModelChecker(QObject, Extension):
if not isinstance(args[0], Camera):
self._change_timer.start()
## Called when plug-ins are initialized.
#
# This makes sure that we listen to changes of the material and that the
# button is created that indicates warnings with the current set-up.
def _pluginsInitialized(self):
"""Called when plug-ins are initialized.
This makes sure that we listen to changes of the material and that the
button is created that indicates warnings with the current set-up.
"""
Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged)
self._createView()
@ -106,8 +108,12 @@ class ModelChecker(QObject, Extension):
if node.callDecoration("isSliceable"):
yield node
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self):
"""Creates the view used by show popup.
The view is saved because of the fairly aggressive garbage collection.
"""
Logger.log("d", "Creating model checker view.")
# Create the plugin dialog component

View File

@ -1,72 +1,72 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from cura.Stages.CuraStage import CuraStage
## Stage for monitoring a 3D printing while it's printing.
class MonitorStage(CuraStage):
def __init__(self, parent = None):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
self._printer_output_device = None
self._active_print_job = None
self._active_printer = None
def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job:
self._active_print_job = print_job
def _setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer:
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
self._active_printer = printer
if self._active_printer:
self._setActivePrintJob(self._active_printer.activePrintJob)
# Jobs might change, so we need to listen to it's changes.
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
else:
self._setActivePrintJob(None)
def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob)
def _onActivePrinterChanged(self):
self._setActivePrinter(self._printer_output_device.activePrinter)
def _onOutputDevicesChanged(self):
try:
# We assume that you are monitoring the device with the highest priority.
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device:
if self._printer_output_device:
try:
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
except TypeError:
# Ignore stupid "Not connected" errors.
pass
self._printer_output_device = new_output_device
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._setActivePrinter(self._printer_output_device.activePrinter)
except IndexError:
pass
def _onEngineCreated(self):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged()
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
if plugin_path is not None:
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
self.addDisplayComponent("menu", menu_component_path)
self.addDisplayComponent("main", main_component_path)
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from cura.Stages.CuraStage import CuraStage
class MonitorStage(CuraStage):
"""Stage for monitoring a 3D printing while it's printing."""
def __init__(self, parent = None):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
self._printer_output_device = None
self._active_print_job = None
self._active_printer = None
def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job:
self._active_print_job = print_job
def _setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer:
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
self._active_printer = printer
if self._active_printer:
self._setActivePrintJob(self._active_printer.activePrintJob)
# Jobs might change, so we need to listen to it's changes.
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
else:
self._setActivePrintJob(None)
def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob)
def _onActivePrinterChanged(self):
self._setActivePrinter(self._printer_output_device.activePrinter)
def _onOutputDevicesChanged(self):
try:
# We assume that you are monitoring the device with the highest priority.
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device:
if self._printer_output_device:
try:
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
except TypeError:
# Ignore stupid "Not connected" errors.
pass
self._printer_output_device = new_output_device
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._setActivePrinter(self._printer_output_device.activePrinter)
except IndexError:
pass
def _onEngineCreated(self):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged()
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
if plugin_path is not None:
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
self.addDisplayComponent("menu", menu_component_path)
self.addDisplayComponent("main", main_component_path)

View File

@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## The per object setting visibility handler ensures that only setting
# definitions that have a matching instance Container are returned as visible.
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
"""The per object setting visibility handler ensures that only setting
definitions that have a matching instance Container are returned as visible.
"""
def __init__(self, parent = None, *args, **kwargs):
super().__init__(parent = parent, *args, **kwargs)

View File

@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance
from UM.Event import Event
## This tool allows the user to add & change settings per node in the scene.
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
class PerObjectSettingsTool(Tool):
"""This tool allows the user to add & change settings per node in the scene.
The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
"""
def __init__(self):
super().__init__()
self._model = None
@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool):
except AttributeError:
return ""
## Gets the active extruder of the currently selected object.
#
# \return The active extruder of the currently selected object.
def getSelectedActiveExtruder(self):
"""Gets the active extruder of the currently selected object.
:return: The active extruder of the currently selected object.
"""
selected_object = Selection.getSelectedObject(0)
return selected_object.callDecoration("getActiveExtruder")
## Changes the active extruder of the currently selected object.
#
# \param extruder_stack_id The ID of the extruder to print the currently
# selected object with.
def setSelectedActiveExtruder(self, extruder_stack_id):
"""Changes the active extruder of the currently selected object.
:param extruder_stack_id: The ID of the extruder to print the currently
selected object with.
"""
selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
selected_object.addDecorator(SettingOverrideDecorator())
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type
def setMeshType(self, mesh_type: str) -> bool:
"""Returns True when the mesh_type was changed, False when current mesh_type == mesh_type"""
old_mesh_type = self.getMeshType()
if old_mesh_type == mesh_type:
return False

View File

@ -27,9 +27,8 @@ if TYPE_CHECKING:
from .Script import Script
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
# g-code files.
class PostProcessingPlugin(QObject, Extension):
"""Extension type plugin that enables pre-written scripts to post process g-code files."""
def __init__(self, parent = None) -> None:
QObject.__init__(self, parent)
Extension.__init__(self)
@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension):
except IndexError:
return ""
## Execute all post-processing scripts on the gcode.
def execute(self, output_device) -> None:
"""Execute all post-processing scripts on the gcode."""
scene = Application.getInstance().getController().getScene()
# If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"):
@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension):
self.selectedIndexChanged.emit() #Ensure that settings are updated
self._propertyChanged()
## Remove a script from the active script list by index.
@pyqtSlot(int)
def removeScriptByIndex(self, index: int) -> None:
"""Remove a script from the active script list by index."""
self._script_list.pop(index)
if len(self._script_list) - 1 < self._selected_script_index:
self._selected_script_index = len(self._script_list) - 1
@ -129,10 +130,12 @@ class PostProcessingPlugin(QObject, Extension):
self.selectedIndexChanged.emit() # Ensure that settings are updated
self._propertyChanged()
## Load all scripts from all paths where scripts can be found.
#
# This should probably only be done on init.
def loadAllScripts(self) -> None:
"""Load all scripts from all paths where scripts can be found.
This should probably only be done on init.
"""
if self._loaded_scripts: # Already loaded.
return
@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension):
self.loadScripts(path)
## Load all scripts from provided path.
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadScripts(self, path: str) -> None:
"""Load all scripts from provided path.
This should probably only be done on init.
:param path: Path to check for scripts.
"""
if ApplicationMetadata.IsEnterpriseVersion:
# Delete all __pycache__ not in installation folder, as it may present a security risk.
@ -173,8 +178,8 @@ class PostProcessingPlugin(QObject, Extension):
if not is_in_installation_path:
TrustBasics.removeCached(path)
## Load all scripts in the scripts folders
scripts = pkgutil.iter_modules(path = [path])
"""Load all scripts in the scripts folders"""
for loader, script_name, ispkg in scripts:
# Iterate over all scripts.
if script_name not in sys.modules:
@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension):
self.scriptListChanged.emit()
self._propertyChanged()
## When the global container stack is changed, swap out the list of active
# scripts.
def _onGlobalContainerStackChanged(self) -> None:
"""When the global container stack is changed, swap out the list of active scripts."""
if self._global_container_stack:
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension):
# We do want to listen to other events.
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self) -> None:
"""Creates the view used by show popup.
The view is saved because of the fairly aggressive garbage collection.
"""
Logger.log("d", "Creating post processing plugin view.")
self.loadAllScripts()
@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension):
# Create the save button component
CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
## Show the (GUI) popup of the post processing plugin.
def showPopup(self) -> None:
"""Show the (GUI) popup of the post processing plugin."""
if self._view is None:
self._createView()
if self._view is None:
@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension):
return
self._view.show()
## Property changed: trigger re-slice
# To do this we use the global container stack propertyChanged.
# Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
def _propertyChanged(self) -> None:
"""Property changed: trigger re-slice
To do this we use the global container stack propertyChanged.
Re-slicing is necessary for setting changes in this plugin, because the changes
are applied only once per "fresh" gcode
"""
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is not None:
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")

View File

@ -23,9 +23,10 @@ if TYPE_CHECKING:
from UM.Settings.Interfaces import DefinitionContainerInterface
## Base class for scripts. All scripts should inherit the script class.
@signalemitter
class Script:
"""Base class for scripts. All scripts should inherit the script class."""
def __init__(self) -> None:
super().__init__()
self._stack = None # type: Optional[ContainerStack]
@ -78,13 +79,15 @@ class Script:
if global_container_stack is not None:
global_container_stack.propertyChanged.emit(key, property_name)
## Needs to return a dict that can be used to construct a settingcategory file.
# See the example script for an example.
# It follows the same style / guides as the Uranium settings.
# Scripts can either override getSettingData directly, or use getSettingDataString
# to return a string that will be parsed as json. The latter has the benefit over
# returning a dict in that the order of settings is maintained.
def getSettingData(self) -> Dict[str, Any]:
"""Needs to return a dict that can be used to construct a settingcategory file.
See the example script for an example.
It follows the same style / guides as the Uranium settings.
Scripts can either override getSettingData directly, or use getSettingDataString
to return a string that will be parsed as json. The latter has the benefit over
returning a dict in that the order of settings is maintained.
"""
setting_data_as_string = self.getSettingDataString()
setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
return setting_data
@ -104,15 +107,18 @@ class Script:
return self._stack.getId()
return None
## Convenience function that retrieves value of a setting from the stack.
def getSettingValueByKey(self, key: str) -> Any:
"""Convenience function that retrieves value of a setting from the stack."""
if self._stack is not None:
return self._stack.getProperty(key, "value")
return None
## Convenience function that finds the value in a line of g-code.
# When requesting key = x from line "G1 X100" the value 100 is returned.
def getValue(self, line: str, key: str, default = None) -> Any:
"""Convenience function that finds the value in a line of g-code.
When requesting key = x from line "G1 X100" the value 100 is returned.
"""
if not key in line or (';' in line and line.find(key) > line.find(';')):
return default
sub_part = line[line.find(key) + 1:]
@ -127,20 +133,23 @@ class Script:
except ValueError: #Not a number at all.
return default
## Convenience function to produce a line of g-code.
#
# You can put in an original g-code line and it'll re-use all the values
# in that line.
# All other keyword parameters are put in the result in g-code's format.
# For instance, if you put ``G=1`` in the parameters, it will output
# ``G1``. If you put ``G=1, X=100`` in the parameters, it will output
# ``G1 X100``. The parameters G and M will always be put first. The
# parameters T and S will be put second (or first if there is no G or M).
# The rest of the parameters will be put in arbitrary order.
# \param line The original g-code line that must be modified. If not
# provided, an entirely new g-code line will be produced.
# \return A line of g-code with the desired parameters filled in.
def putValue(self, line: str = "", **kwargs) -> str:
"""Convenience function to produce a line of g-code.
You can put in an original g-code line and it'll re-use all the values
in that line.
All other keyword parameters are put in the result in g-code's format.
For instance, if you put ``G=1`` in the parameters, it will output
``G1``. If you put ``G=1, X=100`` in the parameters, it will output
``G1 X100``. The parameters G and M will always be put first. The
parameters T and S will be put second (or first if there is no G or M).
The rest of the parameters will be put in arbitrary order.
:param line: The original g-code line that must be modified. If not
provided, an entirely new g-code line will be produced.
:return: A line of g-code with the desired parameters filled in.
"""
#Strip the comment.
comment = ""
if ";" in line:
@ -179,7 +188,9 @@ class Script:
return result
## This is called when the script is executed.
# It gets a list of g-code strings and needs to return a (modified) list.
def execute(self, data: List[str]) -> List[str]:
"""This is called when the script is executed.
It gets a list of g-code strings and needs to return a (modified) list.
"""
raise NotImplementedError()

View File

@ -63,10 +63,12 @@ class FilamentChange(Script):
}
}"""
## Inserts the filament change g-code at specific layer numbers.
# \param data A list of layers of g-code.
# \return A similar list, with filament change commands inserted.
def execute(self, data: List[str]):
"""Inserts the filament change g-code at specific layer numbers.
:param data: A list of layers of g-code.
:return: A similar list, with filament change commands inserted.
"""
layer_nums = self.getSettingValueByKey("layer_number")
initial_retract = self.getSettingValueByKey("initial_retract")
later_retract = self.getSettingValueByKey("later_retract")

View File

@ -134,9 +134,8 @@ class PauseAtHeight(Script):
}
}"""
## Get the X and Y values for a layer (will be used to get X and Y of the
# layer after the pause).
def getNextXY(self, layer: str) -> Tuple[float, float]:
"""Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause)."""
lines = layer.split("\n")
for line in lines:
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
@ -145,10 +144,12 @@ class PauseAtHeight(Script):
return x, y
return 0, 0
## Inserts the pause commands.
# \param data: List of layers.
# \return New list of layers.
def execute(self, data: List[str]) -> List[str]:
"""Inserts the pause commands.
:param data: List of layers.
:return: New list of layers.
"""
pause_at = self.getSettingValueByKey("pause_at")
pause_height = self.getSettingValueByKey("pause_height")
pause_layer = self.getSettingValueByKey("pause_layer")

View File

@ -5,8 +5,10 @@ import math
from ..Script import Script
## Continues retracting during all travel moves.
class RetractContinue(Script):
"""Continues retracting during all travel moves."""
def getSettingDataString(self):
return """{
"name": "Retract Continue",

View File

@ -5,11 +5,14 @@ import re #To perform the search and replace.
from ..Script import Script
## Performs a search-and-replace on all g-code.
#
# Due to technical limitations, the search can't cross the border between
# layers.
class SearchAndReplace(Script):
"""Performs a search-and-replace on all g-code.
Due to technical limitations, the search can't cross the border between
layers.
"""
def getSettingDataString(self):
return """{
"name": "Search and Replace",

View File

@ -1,19 +1,21 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from cura.Stages.CuraStage import CuraStage
## Stage for preparing model (slicing).
class PrepareStage(CuraStage):
def __init__(self, parent = None):
super().__init__(parent)
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
def _engineCreated(self):
menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml")
self.addDisplayComponent("menu", menu_component_path)
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from cura.Stages.CuraStage import CuraStage
class PrepareStage(CuraStage):
"""Stage for preparing model (slicing)."""
def __init__(self, parent = None):
super().__init__(parent)
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
def _engineCreated(self):
menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml")
self.addDisplayComponent("menu", menu_component_path)
self.addDisplayComponent("main", main_component_path)

View File

@ -12,37 +12,45 @@ if TYPE_CHECKING:
from UM.View.View import View
## Displays a preview of what you're about to print.
#
# The Python component of this stage just loads PreviewMain.qml for display
# when the stage is selected, and makes sure that it reverts to the previous
# view when the previous stage is activated.
class PreviewStage(CuraStage):
"""Displays a preview of what you're about to print.
The Python component of this stage just loads PreviewMain.qml for display
when the stage is selected, and makes sure that it reverts to the previous
view when the previous stage is activated.
"""
def __init__(self, application: QtApplication, parent = None) -> None:
super().__init__(parent)
self._application = application
self._application.engineCreatedSignal.connect(self._engineCreated)
self._previously_active_view = None # type: Optional[View]
## When selecting the stage, remember which was the previous view so that
# we can revert to that view when we go out of the stage later.
def onStageSelected(self) -> None:
"""When selecting the stage, remember which was the previous view so that
we can revert to that view when we go out of the stage later.
"""
self._previously_active_view = self._application.getController().getActiveView()
## Called when going to a different stage (away from the Preview Stage).
#
# When going to a different stage, the view should be reverted to what it
# was before. Normally, that just reverts it to solid view.
def onStageDeselected(self) -> None:
"""Called when going to a different stage (away from the Preview Stage).
When going to a different stage, the view should be reverted to what it
was before. Normally, that just reverts it to solid view.
"""
if self._previously_active_view is not None:
self._application.getController().setActiveView(self._previously_active_view.getPluginId())
self._previously_active_view = None
## Delayed load of the QML files.
#
# We need to make sure that the QML engine is running before we can load
# these.
def _engineCreated(self) -> None:
"""Delayed load of the QML files.
We need to make sure that the QML engine is running before we can load
these.
"""
plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId())
if plugin_path is not None:
menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml")

View File

@ -10,12 +10,14 @@ import glob
import os
import subprocess
## Support for removable devices on Linux.
#
# TODO: This code uses the most basic interfaces for handling this.
# We should instead use UDisks2 to handle mount/unmount and hotplugging events.
#
class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
"""Support for removable devices on Linux.
TODO: This code uses the most basic interfaces for handling this.
We should instead use UDisks2 to handle mount/unmount and hotplugging events.
"""
def checkRemovableDrives(self):
drives = {}
for volume in glob.glob("/media/*"):

View File

@ -9,8 +9,10 @@ import os
import plistlib
## Support for removable devices on Mac OSX
class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
"""Support for removable devices on Mac OSX"""
def checkRemovableDrives(self):
drives = {}
p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)

View File

@ -28,17 +28,19 @@ class RemovableDriveOutputDevice(OutputDevice):
self._writing = False
self._stream = None
## Request the specified nodes to be written to the removable drive.
#
# \param nodes A collection of scene nodes that should be written to the
# removable drive.
# \param file_name \type{string} A suggestion for the file name to write
# to. If none is provided, a file name will be made from the names of the
# meshes.
# \param limit_mimetypes Should we limit the available MIME types to the
# MIME types available to the currently active machine?
#
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
"""Request the specified nodes to be written to the removable drive.
:param nodes: A collection of scene nodes that should be written to the
removable drive.
:param file_name: :type{string} A suggestion for the file name to write to.
If none is provided, a file name will be made from the names of the
meshes.
:param limit_mimetypes: Should we limit the available MIME types to the
MIME types available to the currently active machine?
"""
filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do)
if self._writing:
raise OutputDeviceError.DeviceBusyError()
@ -106,14 +108,14 @@ class RemovableDriveOutputDevice(OutputDevice):
Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
## Generate a file name automatically for the specified nodes to be saved
# in.
#
# The name generated will be the name of one of the nodes. Which node that
# is can not be guaranteed.
#
# \param nodes A collection of nodes for which to generate a file name.
def _automaticFileName(self, nodes):
"""Generate a file name automatically for the specified nodes to be saved in.
The name generated will be the name of one of the nodes. Which node that
is can not be guaranteed.
:param nodes: A collection of nodes for which to generate a file name.
"""
for root in nodes:
for child in BreadthFirstIterator(root):
if child.getMeshData():

View File

@ -42,8 +42,9 @@ ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore
## Removable drive support for windows
class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
"""Removable drive support for windows"""
def checkRemovableDrives(self):
drives = {}