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,136 @@
1
+ """In order to facilitate unit testing in CI.
2
+
3
+ we extracted all the interfaces used in Maya and mocked them uniformly.
4
+
5
+ """
6
+
7
+ # Import third-party modules
8
+ try:
9
+ # Import third-party modules
10
+ import maya.api.OpenMaya as om
11
+ import maya.cmds as cmds
12
+ import maya.mel as mel
13
+ except ImportError:
14
+ # Backward compatibility to support test in uinstalled maya.
15
+ try:
16
+ # Import built-in modules
17
+ from unittest.mock import MagicMock
18
+ except ImportError:
19
+ # Import third-party modules
20
+ from mock import MagicMock # noqa: UP026
21
+ cmds = MagicMock()
22
+ om = MagicMock()
23
+ mel = MagicMock()
24
+
25
+ # Import built-in modules
26
+ from functools import wraps
27
+
28
+
29
+ def is_maya_standalone():
30
+ """Check if Maya is running in standalone mode.
31
+
32
+ Returns:
33
+ bool: True if Maya is running in standalone mode, False otherwise.
34
+ """
35
+ return cmds.about(batch=True)
36
+
37
+
38
+ def check_reference_node_exists(node_name):
39
+ """Check if a reference node exists in the Maya scene.
40
+
41
+ Args:
42
+ node_name (str): Name of the reference node.
43
+
44
+ Returns:
45
+ bool: True if the reference node exists, False otherwise.
46
+ """
47
+ try:
48
+ return cmds.referenceQuery(node_name, isNodeReferenced=True)
49
+ except RuntimeError:
50
+ return False
51
+
52
+
53
+ def get_reference_file_by_node(node_name):
54
+ """Get the reference file associated with a node.
55
+
56
+ Args:
57
+ node_name (str): Name of the node.
58
+
59
+ Returns:
60
+ str: Path of the reference file, empty string if the node is not associated with a reference file.
61
+ """
62
+ try:
63
+ return cmds.referenceQuery(node_name, filename=True)
64
+ except RuntimeError:
65
+ return ""
66
+
67
+
68
+ def get_attr_value(node_name, attr_name):
69
+ """Get the value of an attribute of a node.
70
+
71
+ Args:
72
+ node_name (str): Name of the node.
73
+ attr_name (str): Name of the attribute.
74
+
75
+ Returns:
76
+ Any: Value of the attribute, None if the attribute does not exist.
77
+ """
78
+ try:
79
+ return cmds.getAttr("{node_name}.{attr}".format(node_name=node_name, attr=attr_name))
80
+ except ValueError:
81
+ return None
82
+
83
+
84
+ def maya_ui_language():
85
+ """Get the language of the Maya user interface.
86
+
87
+ Returns:
88
+ str: The language of the Maya user interface.
89
+ """
90
+ return cmds.about(uiLocaleLanguage=True)
91
+
92
+
93
+ def block_prompt(func):
94
+ """Decorator to block file prompt dialogs in Maya.
95
+
96
+ Args:
97
+ func (function): The function to decorate.
98
+
99
+ Returns:
100
+ function: The decorated function.
101
+ """
102
+ @wraps(func)
103
+ def wrap(*args, **kwargs):
104
+ # Grabs the initial value.
105
+ prompt_val = cmds.file(prompt=True, q=True)
106
+
107
+ try:
108
+ cmds.file(prompt=False)
109
+ return func(*args, **kwargs)
110
+
111
+ finally:
112
+ # Resets to the original value, this way you don't suddenly turn the prompt on, when someone wanted it off.
113
+ cmds.file(prompt=prompt_val)
114
+
115
+ return wrap
116
+
117
+
118
+ @block_prompt
119
+ def open_maya_file(maya_file):
120
+ """Open a Maya file.
121
+
122
+ Args:
123
+ maya_file (str): Path to the Maya file.
124
+ """
125
+ cmds.file(maya_file, open=True, force=True, ignoreVersion=True, executeScriptNodes=False)
126
+
127
+
128
+ @block_prompt
129
+ def save_as_file(file_name):
130
+ """Save the current Maya scene as a file.
131
+
132
+ Args:
133
+ file_name (str): Path to the output file.
134
+ """
135
+ cmds.file(rename=file_name)
136
+ cmds.file(s=True, f=True)
@@ -0,0 +1,101 @@
1
+ # Import built-in modules
2
+ import glob
3
+ import logging
4
+ import os
5
+ import shutil
6
+
7
+ # Import local modules
8
+ from maya_umbrella import maya_funs
9
+ from maya_umbrella.defender import context_defender
10
+ from maya_umbrella.filesystem import get_backup_path
11
+ from maya_umbrella.filesystem import read_file
12
+ from maya_umbrella.maya_funs import cmds
13
+
14
+
15
+ class MayaVirusScanner(object):
16
+ """A class to scan and fix Maya files containing viruses.
17
+
18
+ Attributes:
19
+ _failed_files (list): List of files that failed to be fixed.
20
+ _fixed_files (list): List of files that have been fixed.
21
+ logger (Logger): Logger object for logging purposes.
22
+ defender (MayaVirusDefender): MayaVirusDefender object for fixing issues.
23
+ _env (dict): Custom environment variables.
24
+ output_path (str, optional): Path to save the fixed files. Defaults to None, which overwrites the original
25
+ files.
26
+ """
27
+
28
+ def __init__(self, output_path=None, env=None):
29
+ """Initialize the MayaVirusScanner.
30
+
31
+ Args:
32
+ output_path (str, optional): Path to save the fixed files. Defaults to None, which overwrites the original
33
+ files.
34
+ env (dict, optional): Custom environment variables. Defaults to None,
35
+ which sets the 'MAYA_COLOR_MANAGEMENT_SYNCOLOR' variable to '1'.
36
+ """
37
+ self.logger = logging.getLogger(__name__)
38
+ self.defender = None
39
+ self.output_path = output_path
40
+ self._failed_files = []
41
+ self._fixed_files = []
42
+ # Custom env.
43
+ self._env = env or {
44
+ "MAYA_COLOR_MANAGEMENT_SYNCOLOR": "1"
45
+ }
46
+
47
+ def scan_files_from_pattern(self, pattern):
48
+ """Scan and fix Maya files matching a given pattern.
49
+
50
+ Args:
51
+ pattern (str): The file pattern to match.
52
+ """
53
+ os.environ.update(self._env)
54
+ return self.scan_files_from_list(glob.iglob(pattern))
55
+
56
+ def scan_files_from_list(self, files):
57
+ """Scan and fix Maya files from a given list.
58
+
59
+ Args:
60
+ files (list): List of file paths to scan and fix.
61
+ """
62
+ with context_defender() as defender:
63
+ self.defender = defender
64
+ for maya_file in files:
65
+ self._fix(maya_file)
66
+ return self._fixed_files
67
+
68
+ def scan_files_from_file(self, text_file):
69
+ """Scan and fix Maya files from a given text file containing a list of file paths.
70
+
71
+ Args:
72
+ text_file (str): Path to the text file containing the list of file paths.
73
+ """
74
+ file_data = read_file(text_file)
75
+ files = file_data.splitlines()
76
+ return self.scan_files_from_list(files)
77
+
78
+ def _fix(self, maya_file):
79
+ """Fix a single Maya file containing a virus.
80
+
81
+ Args:
82
+ maya_file (str): Path to the Maya file to be fixed.
83
+ """
84
+ if not maya_file and maya_file in self._fixed_files:
85
+ self.logger.debug("Already fixed: {maya_file}".format(maya_file=maya_file))
86
+ return
87
+ try:
88
+ maya_funs.open_maya_file(maya_file)
89
+ self.defender.collect()
90
+ except Exception:
91
+ self._failed_files.append(maya_file)
92
+ if self.defender.have_issues:
93
+ self.defender.fix()
94
+ backup_path = get_backup_path(maya_file, root_path=self.output_path)
95
+ self.logger.debug("Backup saved to: {backup_path}".format(backup_path=backup_path))
96
+ shutil.copy2(maya_file, backup_path)
97
+ cmds.file(s=True, f=True)
98
+ self._fixed_files.append(maya_file)
99
+ cmds.file(new=True, force=True)
100
+ for ref in self.defender.collector.infected_reference_files:
101
+ self._fix(ref)
@@ -0,0 +1,10 @@
1
+ # Import built-in modules
2
+ from collections import namedtuple
3
+
4
+
5
+ VirusSignature = namedtuple("VirusSignature", ["name", "signature"])
6
+
7
+ # https://regex101.com/r/0MNzF7/1
8
+ virus20240430_sig1 = VirusSignature("virus20240430", "python(.*);.+exec.+(pyCode).+;")
9
+ # https://regex101.com/r/2D14UA/1
10
+ virus20240430_sig2 = VirusSignature("virus20240430", r"^\['.+']")
maya_umbrella/vaccine.py CHANGED
@@ -1,207 +1,12 @@
1
- # Import built-in modules
2
- from collections import defaultdict
3
- import glob
4
- import logging
5
- import os
6
-
7
- # Import third-party modules
8
- import maya.cmds as cmds
9
-
10
- # Import local modules
11
- from maya_umbrella.filesystem import safe_remove_file
12
- from maya_umbrella.filesystem import safe_rmtree
13
-
14
-
15
- class MayaVirusCleaner(object):
16
- _bad_files = []
17
- _bad_nodes = []
18
- _bad_script_nodes = []
19
- _bad_script_jobs = []
20
- _registered_callbacks = defaultdict(list)
21
- _fix_funcs = []
22
-
23
- def __init__(self, logger=None, auto_fix=True):
24
- self.logger = logger or logging.getLogger(__name__)
25
- self.auto_fix = auto_fix
26
-
27
- @property
28
- def user_app_dir(self):
29
- """Return the user application directory."""
30
- return cmds.internalVar(userAppDir=True)
31
-
32
- @property
33
- def maya_file(self):
34
- """Return the current Maya file."""
35
- return cmds.file(q=True, sn=True)
36
-
37
- @property
38
- def maya_install_root(self):
39
- """Return the Maya installation root directory."""
40
- return os.environ["MAYA_LOCATION"]
41
-
42
- @property
43
- def user_script_path(self):
44
- """Return the user script directory."""
45
- return cmds.internalVar(userScriptDir=True)
46
-
47
- @property
48
- def local_script_path(self):
49
- """Return the local script directory."""
50
- return os.path.join(self.user_app_dir, "scripts")
51
-
52
- @property
53
- def bad_files(self):
54
- """Return a list of bad files."""
55
- return [path for path in list(set(self._bad_files)) if os.path.exists(path)]
56
-
57
- @property
58
- def bad_nodes(self):
59
- """Return a list of bad nodes."""
60
- return list(set(self._bad_nodes))
61
-
62
- @property
63
- def bad_script_nodes(self):
64
- """Return a list of bad script nodes."""
65
- return list(set(self._bad_script_nodes))
66
-
67
- @property
68
- def bad_script_jobs(self):
69
- """Return a list of bad script jobs."""
70
- return list(set(self._bad_script_jobs))
71
-
72
- def callback_remove_rename_temp_files(self, *args, **kwargs):
73
- """
74
- Remove temporary files in the local script path.
75
- """
76
- self.logger.info("Removing temporary files in %s", self.local_script_path)
77
- [safe_remove_file(temp_file) for temp_file in glob.glob(os.path.join(self.local_script_path, "._*"))]
78
-
79
- @property
80
- def registered_callbacks(self):
81
- return self._registered_callbacks
82
-
83
- def add_bad_files(self, files):
84
- self._bad_files.extend(files)
85
-
86
- def add_bad_file(self, file):
87
- self._bad_files.append(file)
88
-
89
- def add_bad_nodes(self, nodes):
90
- self._bad_nodes.extend(nodes)
91
-
92
- def add_bad_node(self, node):
93
- self._bad_nodes.append(node)
94
-
95
- def add_bad_script_jobs(self, jobs):
96
- self._bad_script_jobs.extend(jobs)
97
-
98
- def add_bad_script_job(self, job):
99
- self._bad_script_jobs.append(job)
100
-
101
- def add_bad_script_nodes(self, nodes):
102
- self._bad_script_nodes.extend(nodes)
103
-
104
- def add_bad_script_node(self, node):
105
- self._bad_script_nodes.append(node)
106
-
107
- def register_callback(self, callback_name, callback):
108
- """Register a callback to be executed before or after processing."""
109
- self._registered_callbacks[callback_name].append(callback)
110
-
111
- def add_after_open_callback(self, callback):
112
- self.register_callback("after_open", callback)
113
-
114
- def add_maya_initialized_callback(self, callback):
115
- self.register_callback("maya_initialized", callback)
116
-
117
- def add_after_import_callback(self, callback):
118
- self.register_callback("after_import", callback)
119
-
120
- def add_after_import_reference_callback(self, callback):
121
- self.register_callback("after_import_reference", callback)
122
-
123
- def add_after_load_reference_callback(self, callback):
124
- self.register_callback("after_load_reference", callback)
125
-
126
- def add_before_save_callback(self, callback):
127
- self.register_callback("before_save", callback)
128
-
129
- def add_before_import_callback(self, callback):
130
- self.register_callback("before_import", callback)
131
-
132
- def add_before_load_reference_callback(self, callback):
133
- self.register_callback("before_load_reference", callback)
134
-
135
- def add_before_import_reference_callback(self, callback):
136
- self.register_callback("before_import_reference", callback)
137
-
138
- def add_maya_exiting_callback(self, callback):
139
- self.register_callback("maya_exiting", callback)
140
-
141
- def setup_default_callbacks(self):
142
- self.add_maya_initialized_callback(self.callback_remove_rename_temp_files)
143
- self.add_maya_exiting_callback(self.callback_remove_rename_temp_files)
144
-
145
- def add_fix_function(self, func):
146
- self._fix_funcs.append(func)
147
-
148
- # fix
149
-
150
- def fix_script_jobs(self):
151
- for script_job in self.bad_script_jobs:
152
- script_num = int(script_job.split(":", 1)[0])
153
- self.logger.info("Kill script job %s", script_job)
154
- cmds.scriptJob(kill=script_num, force=True)
155
- self._bad_script_jobs.remove(script_job)
156
-
157
- def fix_bad_files(self):
158
- for file_ in self.bad_files:
159
- if os.path.exists(file_):
160
- if os.path.isfile(file_):
161
- self.logger.info("Removing %s", file_)
162
- safe_remove_file(file_)
163
- self._bad_files.remove(file_)
164
- else:
165
- self.logger.info("Removing folder %s", file_)
166
- safe_rmtree(file_)
167
- self._bad_files.remove(file_)
168
-
169
- def fix_bad_nodes(self):
170
- for node in self.bad_nodes:
171
- self.logger.info("Deleting %s", node)
172
- try:
173
- cmds.lockNode(node, lock=False)
174
- except ValueError:
175
- pass
176
- try:
177
- cmds.delete(node)
178
- except ValueError:
179
- pass
180
- self._bad_nodes.remove(node)
181
-
182
- def fix_all_issues(self):
183
- """Fix all issues related to the Maya virus."""
184
- self.fix_bad_files()
185
- self.fix_bad_nodes()
186
- self.fix_script_jobs()
187
- for func in self._fix_funcs:
188
- func()
189
-
190
- def report_all_issues(self):
191
- """Report all issues related to the Maya virus."""
192
- self.logger.info("Bad files: %s", self.bad_files)
193
- self.logger.info("Bad nodes: %s", self.bad_nodes)
194
- self.logger.info("Bad script jobs: %s", self.bad_script_jobs)
195
-
196
- def reset_all_issues(self):
197
- """Reset all issues related to the Maya virus."""
198
- self._bad_files = []
199
- self._bad_nodes = []
200
- self._bad_script_nodes = []
201
- self._bad_script_jobs = []
1
+ class AbstractVaccine(object):
2
+ """Abstract base class for Vaccine classes.
202
3
 
4
+ Attributes:
5
+ virus_name (str): The name of the virus.
6
+ api (MayaVirusCleaner): The VaccineAPI instance.
7
+ logger (Logger): The logger instance.
8
+ """
203
9
 
204
- class AbstractVaccine(object):
205
10
  virus_name = None
206
11
 
207
12
  def __init__(self, api, logger):
@@ -216,7 +21,17 @@ class AbstractVaccine(object):
216
21
  self.logger = logger
217
22
 
218
23
  def collect_issues(self):
24
+ """Collect issues related to the virus.
25
+
26
+ Raises:
27
+ NotImplementedError: This method must be implemented in the derived classes.
28
+ """
219
29
  raise NotImplementedError
220
30
 
221
31
  def report_issue(self, name):
222
- self.logger.warning("%s: Infected by Malware!", name)
32
+ """Report an issue related to the virus.
33
+
34
+ Args:
35
+ name (str): The name of the issue.
36
+ """
37
+ self.logger.warning(self.api.translator.translate("report_issue", name=name))
@@ -6,10 +6,11 @@ from maya_umbrella.vaccine import AbstractVaccine
6
6
 
7
7
 
8
8
  class Vaccine(AbstractVaccine):
9
- virus_name = "pu tian tong qi"
9
+ """A class for handling the PuTianTongQi virus."""
10
+ virus_name = "PutTianTongQi"
10
11
 
11
12
  def collect_issues(self):
12
- self.api.add_bad_files(
13
+ self.api.add_malicious_files(
13
14
  [
14
15
  os.path.join(self.api.local_script_path, "fuckVirus.py"),
15
16
  os.path.join(self.api.local_script_path, "fuckVirus.pyc"),
@@ -1,60 +1,52 @@
1
1
  # Import built-in modules
2
2
  import os.path
3
3
 
4
- # Import third-party modules
5
- import maya.cmds as cmds
6
-
7
4
  # Import local modules
8
- from maya_umbrella.filesystem import read_file
9
- from maya_umbrella.filesystem import rename
5
+ from maya_umbrella.constants import JOB_SCRIPTS_VIRUS_SIGNATURES
6
+ from maya_umbrella.filesystem import check_virus_by_signature
7
+ from maya_umbrella.filesystem import check_virus_file_by_signature
8
+ from maya_umbrella.maya_funs import check_reference_node_exists
9
+ from maya_umbrella.maya_funs import cmds
10
+ from maya_umbrella.maya_funs import get_attr_value
10
11
  from maya_umbrella.vaccine import AbstractVaccine
11
12
 
12
13
 
13
14
  class Vaccine(AbstractVaccine):
15
+ """A class for handling the ZeiJianKang virus."""
16
+
14
17
  virus_name = "zei jian kang"
15
18
 
16
- def collect_bad_nodes(self):
19
+ def collect_infected_nodes(self):
17
20
  """Collect all bad nodes related to the virus."""
18
21
  for script_node in cmds.ls(type="script"):
19
- if cmds.referenceQuery(script_node, isNodeReferenced=True):
22
+ if check_reference_node_exists(script_node):
20
23
  continue
21
- script_before_string = cmds.getAttr("{}.before".format(script_node))
22
- script_after_string = cmds.getAttr("{}.after".format(script_node))
23
- for script_string in [script_before_string, script_after_string]:
24
+ for attr_name in ("before", "after"):
25
+ script_string = get_attr_value(script_node, attr_name)
24
26
  if not script_string:
25
27
  continue
26
- if "internalVar" in script_string or "userSetup" in script_string or "fuckVirus" in script_string:
28
+ if check_virus_by_signature(script_string, JOB_SCRIPTS_VIRUS_SIGNATURES):
27
29
  self.report_issue(script_node)
28
- self.api.add_bad_node(script_node)
30
+ self.api.add_infected_node(script_node)
29
31
 
30
32
  def collect_issues(self):
31
33
  """Collect all issues related to the virus."""
32
- self.api.add_bad_files(
34
+ self.api.add_malicious_files(
33
35
  [
34
36
  os.path.join(self.api.local_script_path, "vaccine.py"),
35
37
  os.path.join(self.api.local_script_path, "vaccine.pyc"),
36
38
  ],
37
39
  )
38
- self.collect_bad_usersetup_py()
39
- self.collect_bad_nodes()
40
+ self.collect_infected_user_setup_py()
41
+ self.collect_infected_nodes()
40
42
 
41
- def collect_bad_usersetup_py(self):
43
+ def collect_infected_user_setup_py(self):
42
44
  """Collect all bad userSetup.py files related to the virus."""
43
- for usersetup_py in [
44
- os.path.join(self.api.local_script_path, "vaccine.py"),
45
- os.path.join(self.api.user_script_path, "vaccine.py"),
45
+ for user_setup_py in [
46
46
  os.path.join(self.api.local_script_path, "userSetup.py"),
47
47
  os.path.join(self.api.user_script_path, "userSetup.py"),
48
48
  ]:
49
- if os.path.exists(usersetup_py):
50
- data = read_file(usersetup_py)
51
- if "petri_dish_path = cmds.internalVar(userAppDir=True) + 'scripts/userSetup.py" in data:
52
- self.report_issue("vaccine.py")
53
- self.api.add_bad_file(rename(usersetup_py))
54
-
55
- if (
56
- "cmds.evalDeferred('leukocyte = vaccine.phage()')" in data
57
- and "cmds.evalDeferred('leukocyte.occupation()')" in data
58
- ):
59
- self.report_issue("userSetup.py")
60
- self.api.add_bad_file(rename(usersetup_py))
49
+ if os.path.exists(user_setup_py):
50
+ if check_virus_file_by_signature(user_setup_py):
51
+ self.report_issue(user_setup_py)
52
+ self.api.add_infected_file(user_setup_py)