maya-umbrella 0.4.1__py2.py3-none-any.whl → 0.6.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of maya-umbrella might be problematic. Click here for more details.

@@ -0,0 +1,161 @@
1
+ # Import built-in modules
2
+ from contextlib import contextmanager
3
+ import logging
4
+
5
+ # Import local modules
6
+ from maya_umbrella.cleaner import MayaVirusCleaner
7
+ from maya_umbrella.collector import MayaVirusCollector
8
+ from maya_umbrella.filesystem import get_hooks
9
+ from maya_umbrella.filesystem import load_hook
10
+ from maya_umbrella.i18n import Translator
11
+ from maya_umbrella.log import setup_logger
12
+ from maya_umbrella.maya_funs import is_maya_standalone
13
+ from maya_umbrella.maya_funs import om
14
+
15
+
16
+ # Global list to store IDs of Maya callbacks
17
+ MAYA_UMBRELLA_CALLBACK_IDS = []
18
+
19
+
20
+ def _add_callbacks_id(id_):
21
+ """Add a callback ID to the global list if it's not already present.
22
+
23
+ Args:
24
+ id_ (int): ID of the callback to be added.
25
+ """
26
+ global MAYA_UMBRELLA_CALLBACK_IDS
27
+ if id_ not in MAYA_UMBRELLA_CALLBACK_IDS:
28
+ MAYA_UMBRELLA_CALLBACK_IDS.append(id_)
29
+
30
+
31
+ class MayaVirusDefender(object):
32
+ """A class to defend against Maya viruses.
33
+
34
+ Attributes:
35
+ _vaccines (list): List to store vaccines.
36
+ callback_maps (dict): Dictionary to map callback names to MSceneMessage constants.
37
+ auto_fix (bool): Whether to automatically fix issues.
38
+ logger (Logger): Logger object for logging purposes.
39
+ translator (Translator): Translator object for translation purposes.
40
+ collector (MayaVirusCollector): MayaVirusCollector object for collecting issues.
41
+ virus_cleaner (MayaVirusCleaner): MayaVirusCleaner object for fixing issues.
42
+ hooks (list): List of hooks to run.
43
+ """
44
+ _vaccines = []
45
+ callback_maps = {
46
+ "after_open": om.MSceneMessage.kAfterOpen,
47
+ "maya_initialized": om.MSceneMessage.kMayaInitialized,
48
+ "after_import": om.MSceneMessage.kAfterImport,
49
+ "after_import_reference": om.MSceneMessage.kAfterImportReference,
50
+ "after_load_reference": om.MSceneMessage.kAfterLoadReference,
51
+ "before_save": om.MSceneMessage.kBeforeSave,
52
+ "before_import": om.MSceneMessage.kBeforeImport,
53
+ "before_load_reference": om.MSceneMessage.kBeforeLoadReference,
54
+ "before_import_reference": om.MSceneMessage.kBeforeImportReference,
55
+ "maya_exiting": om.MSceneMessage.kMayaExiting,
56
+ }
57
+
58
+ def __init__(self, auto_fix=True):
59
+ """Initialize the MayaVirusDefender.
60
+
61
+ Args:
62
+ auto_fix (bool): Whether to automatically fix issues.
63
+ """
64
+ logger = logging.getLogger(__name__)
65
+ self.auto_fix = auto_fix
66
+ self.logger = setup_logger(logger)
67
+ self.translator = Translator()
68
+ self.collector = MayaVirusCollector(self.logger, self.translator)
69
+ self.virus_cleaner = MayaVirusCleaner(self.collector, self.logger)
70
+ self.hooks = get_hooks()
71
+
72
+ def run_hooks(self):
73
+ """Run all hooks, only works in non-batch mode."""
74
+ if not is_maya_standalone():
75
+ for hook_file in self.hooks:
76
+ self.logger.debug("run_hook: %s", hook_file)
77
+ try:
78
+ load_hook(hook_file).hook(virus_cleaner=self.virus_cleaner)
79
+ except Exception as e:
80
+ self.logger.debug("Error running hook: %s", e)
81
+
82
+ def collect(self):
83
+ """Collect all issues related to the Maya virus."""
84
+ self.collector.collect()
85
+
86
+ def fix(self):
87
+ """Fix all issues related to the Maya virus."""
88
+ self.virus_cleaner.fix()
89
+
90
+ def report(self):
91
+ """Report all issues related to the Maya virus."""
92
+ self.collect()
93
+ self.collector.report()
94
+
95
+ @property
96
+ def have_issues(self):
97
+ """Check if any issues are found.
98
+
99
+ Returns:
100
+ bool: True if any issues are found, False otherwise.
101
+ """
102
+ return self.collector.have_issues
103
+
104
+ def setup(self):
105
+ """Set up the MayaVirusDefender."""
106
+ self.virus_cleaner.setup_default_callbacks()
107
+ for name, callbacks in self.collector.registered_callbacks.items():
108
+ maya_callback = self.callback_maps[name]
109
+ self.logger.debug("%s setup.", name)
110
+ for func in callbacks:
111
+ _add_callbacks_id(om.MSceneMessage.addCallback(maya_callback, func))
112
+ for name, callbacks in self.callback_maps.items():
113
+ self.logger.debug("setup callback %s.", name)
114
+ _add_callbacks_id(om.MSceneMessage.addCallback(callbacks, self._callback))
115
+
116
+ def stop(self):
117
+ """Stop the MayaVirusDefender."""
118
+ for ids in MAYA_UMBRELLA_CALLBACK_IDS:
119
+ self.logger.debug("remove callback. %s", ids)
120
+ om.MSceneMessage.removeCallback(ids)
121
+ MAYA_UMBRELLA_CALLBACK_IDS.remove(ids)
122
+
123
+ def get_unfixed_references(self):
124
+ """Get the list of unfixed reference files.
125
+
126
+ Returns:
127
+ list: List of unfixed reference files.
128
+ """
129
+ self.collect()
130
+ return self.collector.infected_reference_files
131
+
132
+ def _callback(self, *args, **kwargs):
133
+ """Callback function for MayaVirusDefender.
134
+
135
+ Args:
136
+ *args: Variable length argument list.
137
+ **kwargs: Arbitrary keyword arguments.
138
+ """
139
+ if self.auto_fix:
140
+ self.collect()
141
+ self.fix()
142
+ self.run_hooks()
143
+ else:
144
+ self.report()
145
+
146
+ def start(self):
147
+ """Start the MayaVirusDefender."""
148
+ self._callback()
149
+
150
+
151
+ @contextmanager
152
+ def context_defender():
153
+ """Context manager for MayaVirusDefender.
154
+
155
+ Yields:
156
+ MayaVirusDefender: An instance of MayaVirusDefender.
157
+ """
158
+ defender = MayaVirusDefender()
159
+ defender.stop()
160
+ yield defender
161
+ defender.setup()
@@ -1,17 +1,25 @@
1
1
  # Import built-in modules
2
+ from contextlib import contextmanager
2
3
  import glob
3
4
  import importlib
4
- import io
5
+ import json
5
6
  import os
6
7
  import random
8
+ import re
7
9
  import shutil
8
10
  import string
11
+ import sys
9
12
  import tempfile
10
13
 
11
14
  # Import local modules
15
+ from maya_umbrella.constants import FILE_VIRUS_SIGNATURES
12
16
  from maya_umbrella.constants import PACKAGE_NAME
13
17
 
14
18
 
19
+ PY2 = sys.version_info[0] == 2
20
+ PY3 = sys.version_info[0] == 3
21
+
22
+
15
23
  def this_root():
16
24
  """Return the absolute path of the current file's directory."""
17
25
  return os.path.abspath(os.path.dirname(__file__))
@@ -21,7 +29,7 @@ def safe_remove_file(file_path):
21
29
  """Remove the file at the given path without raising an error if the file does not exist."""
22
30
  try:
23
31
  os.remove(file_path)
24
- except OSError:
32
+ except (OSError, IOError): # noqa: UP024
25
33
  pass
26
34
 
27
35
 
@@ -29,42 +37,65 @@ def safe_rmtree(path):
29
37
  """Remove the directory at the given path without raising an error if the directory does not exist."""
30
38
  try:
31
39
  shutil.rmtree(path)
32
- except OSError:
40
+ except (OSError, IOError): # noqa: UP024
33
41
  pass
34
42
 
35
43
 
36
44
  def read_file(path):
37
45
  """Read the content of the file at the given path."""
38
- with io.open(path, "r", encoding="utf-8") as file_:
39
- content = file_.read()
46
+ options = {"encoding": "utf-8"} if PY3 else {}
47
+ with open(path, **options) as file_:
48
+ try:
49
+ content = file_.read()
50
+ # maya-2022 UnicodeDecodeError from `plug-ins/mayaHIK.pres.mel`
51
+ except UnicodeDecodeError:
52
+ return ""
53
+ return content
54
+
55
+
56
+ def read_json(path):
57
+ """Read the content of the file at the given path."""
58
+ options = {"encoding": "utf-8"} if PY3 else {}
59
+ with open(path, **options) as file_:
60
+ try:
61
+ content = json.load(file_)
62
+ except UnicodeDecodeError:
63
+ return {}
40
64
  return content
41
65
 
42
66
 
43
67
  def write_file(path, content):
44
68
  """Write the given content to the file at the given path."""
45
- with io.open(path, "w", encoding="utf-8", newline="\n") as file_:
69
+ options = {"encoding": "utf-8"} if PY3 else {}
70
+ with atomic_writes(path, "w", **options) as file_:
46
71
  file_.write(content)
47
72
 
48
73
 
49
- def patch_file(source, target, key_values, report_error=True):
50
- """Modify the file at the source path with the given key value pairs and write the result to the target path.
74
+ @contextmanager
75
+ def atomic_writes(src, mode, **options):
76
+ """Context manager for atomic writes to a file.
51
77
 
52
- If report_error is True, raise an IndexError if any of the keys are not found in the source file.
78
+ This context manager ensures that the file is only written to disk if the write operation completes without errors.
79
+
80
+ Args:
81
+ src (str): Path to the file to be written.
82
+ mode (str): Mode in which the file is opened, like 'r', 'w', 'a', etc.
83
+ **options: Arbitrary keyword arguments that are passed to the built-in open() function.
84
+
85
+ Yields:
86
+ file object: The opened file object.
87
+
88
+ Raises:
89
+ AttributeError: If the os module does not have the 'replace' function (Python 2 compatibility).
53
90
  """
54
- key_values = key_values if key_values else {}
55
- found_keys = []
56
- file_data = read_file(source)
57
- for key, value in key_values.items():
58
- before_ = file_data
59
- file_data = file_data.replace(key, value)
60
- if before_ != file_data:
61
- found_keys.append(key)
62
- write_file(target, file_data)
63
-
64
- if report_error:
65
- not_found = list(set(key_values.keys()) - set(found_keys))
66
- if not_found:
67
- raise IndexError("Not found: {0}".format(not_found))
91
+ temp_path = os.path.join(os.path.dirname(src), "._{}".format(id_generator()))
92
+ with open(temp_path, mode, **options) as f:
93
+ yield f
94
+ try:
95
+ os.replace(temp_path, src)
96
+ except AttributeError:
97
+ shutil.move(temp_path, src)
98
+
68
99
 
69
100
 
70
101
  def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
@@ -77,7 +108,7 @@ def rename(src):
77
108
  dst = os.path.join(os.path.dirname(src), "._{}".format(id_generator()))
78
109
  try:
79
110
  os.rename(src, dst)
80
- except OSError:
111
+ except (OSError, IOError): # noqa: UP024
81
112
  return src
82
113
  return dst
83
114
 
@@ -88,7 +119,7 @@ def load_hook(hook_file):
88
119
  if hasattr(importlib, "machinery"):
89
120
  # Python 3
90
121
  # Import built-in modules
91
- from importlib.util import spec_from_loader
122
+ from importlib.util import spec_from_loader # noqa: F401
92
123
 
93
124
  loader = importlib.machinery.SourceFileLoader(hook_name, hook_file)
94
125
  spec = importlib.util.spec_from_loader(loader.name, loader=loader)
@@ -137,7 +168,101 @@ def get_log_file():
137
168
  root = get_log_root()
138
169
  try:
139
170
  os.makedirs(root)
140
- except OSError:
171
+ except (OSError, IOError): # noqa: UP024
141
172
  pass
142
173
  name = os.getenv("MAYA_UMBRELLA_LOG_NAME", PACKAGE_NAME)
143
174
  return os.path.join(root, "{name}.log".format(name=name))
175
+
176
+
177
+ def remove_virus_file_by_signature(file_path, signatures, output_file_path=None):
178
+ """Remove virus content from a file by matching signatures.
179
+
180
+ Args:
181
+ file_path (str): Path to the file to be cleaned.
182
+ signatures (list): List of signatures to match and remove.
183
+ output_file_path (str, optional): Path to the cleaned output file.
184
+ Defaults to None, which overwrites the input file.
185
+ """
186
+ data = read_file(file_path)
187
+ if check_virus_by_signature(data, signatures):
188
+ fixed_data = replace_content_by_signatures(data, signatures)
189
+ write_file(output_file_path or file_path, fixed_data)
190
+
191
+
192
+ def replace_content_by_signatures(content, signatures):
193
+ """Replace content in a string that matches given signatures.
194
+
195
+ Args:
196
+ content (str): The input content.
197
+ signatures (list): List of signatures to match and remove.
198
+
199
+ Returns:
200
+ str: The cleaned content.
201
+ """
202
+ for signature in signatures:
203
+ content = re.sub(signature, "", content)
204
+ return content
205
+
206
+
207
+ def check_virus_file_by_signature(file_path, signatures=None):
208
+ """Check if a file contains a virus by matching signatures.
209
+
210
+ Args:
211
+ file_path (str): Path to the file to be checked.
212
+ signatures (list, optional): List of signatures to match. Defaults to None, which uses FILE_VIRUS_SIGNATURES.
213
+
214
+ Returns:
215
+ bool: True if a virus signature is found, False otherwise.
216
+ """
217
+ signatures = signatures or FILE_VIRUS_SIGNATURES
218
+ try:
219
+ data = read_file(file_path)
220
+ return check_virus_by_signature(data, signatures)
221
+ except (OSError, IOError): # noqa: UP024
222
+ return False
223
+ except UnicodeDecodeError:
224
+ return True
225
+
226
+
227
+ def check_virus_by_signature(content, signatures=None):
228
+ """Check if a content contains a virus by matching signatures.
229
+
230
+ Args:
231
+ content (str): The input content.
232
+ signatures (list, optional): List of signatures to match. Defaults to None, which uses FILE_VIRUS_SIGNATURES.
233
+
234
+ Returns:
235
+ bool: True if a virus signature is found, False otherwise.
236
+ """
237
+ signatures = signatures or FILE_VIRUS_SIGNATURES
238
+ for signature in signatures:
239
+ if re.search(signature, content):
240
+ return True
241
+ return False
242
+
243
+
244
+ def get_backup_path(path, root_path=None):
245
+ """Get the backup path for a given file path based on environment variables.
246
+
247
+ Args:
248
+ path (str): Path to the original file.
249
+ root_path (str, optional): Path to the root folder where backups should be saved.
250
+ Defaults to None, which saves backups in the original file's folder.
251
+
252
+ Returns:
253
+ str: The backup path.
254
+ """
255
+ ignore_backup = os.getenv("MAYA_UMBRELLA_IGNORE_BACKUP", "false").lower() == "true"
256
+ if ignore_backup:
257
+ return path
258
+ root, filename = os.path.split(path)
259
+ backup_folder_name = os.getenv("MAYA_UMBRELLA_BACKUP_FOLDER_NAME", "_virus")
260
+ backup_path = os.path.join(root, backup_folder_name)
261
+ if root_path:
262
+ _, base_path = os.path.splitdrive(root)
263
+ backup_path = os.path.join(root_path, base_path.strip(os.sep))
264
+ try:
265
+ os.makedirs(backup_path)
266
+ except (OSError, IOError): # noqa: UP024
267
+ pass
268
+ return os.path.join(backup_path, filename)
@@ -1,6 +1,6 @@
1
- # Import third-party modules
2
- import maya.cmds as cmds
3
- import maya.mel as mel
1
+ # Import local modules
2
+ from maya_umbrella.maya_funs import cmds
3
+ from maya_umbrella.maya_funs import mel
4
4
 
5
5
 
6
6
  def hook(virus_cleaner):
@@ -1,5 +1,5 @@
1
- # Import third-party modules
2
- import maya.cmds as cmds
1
+ # Import local modules
2
+ from maya_umbrella.maya_funs import cmds
3
3
 
4
4
 
5
5
  def hook(virus_cleaner):
@@ -16,7 +16,8 @@ def hook(virus_cleaner):
16
16
  cmds.lockNode(nodeObj, lock=False)
17
17
  except Exception:
18
18
  virus_cleaner.logger.warning(
19
- "The node is locked and cannot be unlocked. skip {}".format(nodeObj))
19
+ "The node is locked and cannot be unlocked. skip {}".format(nodeObj)
20
+ )
20
21
  continue
21
22
  try:
22
23
  cmds.delete(nodeObj)
@@ -1,5 +1,5 @@
1
- # Import third-party modules
2
- import maya.cmds as cmds
1
+ # Import local modules
2
+ from maya_umbrella.maya_funs import cmds
3
3
 
4
4
 
5
5
  def hook(virus_cleaner):
@@ -1,6 +1,6 @@
1
- # Import third-party modules
2
- import maya.cmds as cmds
3
- import maya.mel as mel
1
+ # Import local modules
2
+ from maya_umbrella.maya_funs import cmds
3
+ from maya_umbrella.maya_funs import mel
4
4
 
5
5
 
6
6
  def hook(virus_cleaner):
maya_umbrella/i18n.py ADDED
@@ -0,0 +1,76 @@
1
+ # Import built-in modules
2
+ import glob
3
+ import os
4
+ from string import Template
5
+
6
+ # Import local modules
7
+ from maya_umbrella.filesystem import read_json
8
+ from maya_umbrella.filesystem import this_root
9
+ from maya_umbrella.maya_funs import maya_ui_language
10
+
11
+
12
+ class Translator(object):
13
+ """A class to handle translations for different locales.
14
+
15
+ Attributes:
16
+ data (dict): Dictionary containing translation data for different locales.
17
+ locale (str): The current locale.
18
+ """
19
+ def __init__(self, file_format="json", default_locale=None):
20
+ """Initialize the Translator.
21
+
22
+ Args:
23
+ file_format (str, optional): File format of the translation files. Defaults to "json".
24
+ default_locale (str, optional): Default locale to use for translations. Defaults to None,
25
+ which uses the MAYA_UMBRELLA_LANG environment variable or the Maya UI language.
26
+ """
27
+ _default_locale = os.getenv("MAYA_UMBRELLA_LANG", maya_ui_language())
28
+ default_locale = default_locale or _default_locale
29
+ self.data = {}
30
+ self.locale = default_locale
31
+ translations_folder = os.path.join(this_root(), "locales")
32
+
33
+ # get list of files with specific extensions
34
+ files = glob.glob(os.path.join(translations_folder, "*.{file_format}".format(file_format=file_format)))
35
+ for fil in files:
36
+ # get the name of the file without extension, will be used as locale name
37
+ loc = os.path.splitext(os.path.basename(fil))[0]
38
+ self.data[loc] = read_json(fil)
39
+
40
+ def set_locale(self, locale):
41
+ """Set the current locale.
42
+
43
+ Args:
44
+ locale (str): The locale to set.
45
+
46
+ Raises:
47
+ ValueError: If the provided locale is not supported.
48
+ """
49
+ if locale in self.data:
50
+ self.locale = locale
51
+ else:
52
+ raise ValueError("Invalid locale: {loc}".format(loc=locale))
53
+
54
+ def get_locale(self):
55
+ """Get the current locale.
56
+
57
+ Returns:
58
+ str: The current locale.
59
+ """
60
+ return self.locale
61
+
62
+ def translate(self, key, **kwargs):
63
+ """Translate a text based on the current locale.
64
+
65
+ Args:
66
+ key (str): The key to be translated.
67
+ **kwargs: Arbitrary keyword arguments that are used to replace placeholders in the translation text.
68
+
69
+ Returns:
70
+ str: The translated text.
71
+ """
72
+ # return the key instead of translation text if locale is not supported
73
+ if self.locale not in self.data:
74
+ return key
75
+ text = self.data[self.locale].get(key, key)
76
+ return Template(text).safe_substitute(**kwargs)
@@ -0,0 +1,19 @@
1
+ {
2
+ "start_fix_issues": "Start fixing all problems related to Maya virus $name",
3
+ "finish_fix_issues": "Done.",
4
+ "init_message": "Successfully loaded <hl>maya_umbrella</hl> under protection.",
5
+ "init_standalone_message": "-----------------------Loading maya_umbrella successfully----------------------",
6
+ "report_issue": "$name: Infected by Malware!",
7
+ "infected_nodes": "Infected nodes: $name: ",
8
+ "bad_files": "Bad files: $name",
9
+ "infected_script_jobs": "Infected script jobs: $name",
10
+ "infected_files": "Infected files: $name",
11
+ "infected_reference_files": "Infected reference files: $name",
12
+ "fix_infected_files": "Clean infected files: $name",
13
+ "fix_infected_nodes": "Delete infected nodes:$name",
14
+ "fix_infected_reference_nodes": "trying fix infected nodes from reference:$name",
15
+ "delete": "Deleting: $name",
16
+ "remove_file": "Deleting file:$name",
17
+ "remove_path": "Deleting path:$name",
18
+ "fix_script_job": "Kill script job: %s"
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "start_fix_issues": "开始修复与 Maya 病毒相关的所有问题 $name",
3
+ "finish_fix_issues": "修复结束.",
4
+ "init_message": "成功加载 <hl>maya_umbrella</hl> 保护中.",
5
+ "init_standalone_message": "-----------------------成功加载 maya_umbrella-----------------------",
6
+ "report_issue": "$name:被恶意感染!",
7
+ "infected_nodes": "被感染节点:$name: ",
8
+ "bad_files": "需要被清理的文件:$name",
9
+ "infected_script_jobs": "被感染的script jobs:$name",
10
+ "infected_files": "被感染的脚本:$name",
11
+ "infected_reference_files": "被感染的参考文件:$name",
12
+ "fix_infected_files": "清理被感染的文件:$name",
13
+ "fix_infected_nodes": "删除被感染的节点:$name",
14
+ "fix_infected_reference_nodes": "尝试修复被感染的参考节点:$name",
15
+ "delete": "删除感染文件:$name",
16
+ "remove_file": "删除文件:$name",
17
+ "remove_path": "删除文件夹:$name",
18
+ "fix_script_job": "删除被感染的节点:$name"
19
+ }