mujoco-toolbox 0.2.0__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mujoco-toolbox
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A modern package to handle MuJoCo environments
5
5
  License: MIT
6
6
  Keywords: MuJoCo,robotics,simulation,toolbox,wrapper
@@ -10,11 +10,7 @@ Requires-Python: >=3.10
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
14
13
  Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
14
  Classifier: Topic :: Software Development :: Libraries
19
15
  Requires-Dist: colorama
20
16
  Requires-Dist: ffmpeg
@@ -29,15 +25,16 @@ Requires-Dist: screeninfo
29
25
  Requires-Dist: tqdm
30
26
  Requires-Dist: trimesh
31
27
  Project-URL: Bug Tracker, https://github.com/MGross21/mujoco-toolbox/issues
32
- Project-URL: documentation, https://github.com/MGross21/mujoco-toolbox
33
- Project-URL: homepage, https://github.com/MGross21/mujoco-toolbox
34
- Project-URL: repository, https://github.com/MGross21/mujoco-toolbox
28
+ Project-URL: Documentation, https://github.com/MGross21/mujoco-toolbox
29
+ Project-URL: Homepage, https://github.com/MGross21/mujoco-toolbox
30
+ Project-URL: Repository, https://github.com/MGross21/mujoco-toolbox
35
31
  Description-Content-Type: text/markdown
36
32
 
37
33
  ![Build](https://github.com/MGross21/mujoco-toolbox/actions/workflows/ci.yml/badge.svg)
38
34
  ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
39
35
  ![License](https://img.shields.io/github/license/MGross21/mujoco-toolbox)
40
36
  [![PyPI](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml)
37
+ [![Docs](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml)
41
38
 
42
39
  # Mujoco Toolbox
43
40
 
@@ -96,28 +93,26 @@ ffmpeg -version
96
93
 
97
94
  </details>
98
95
 
99
- ## To Run in Scripts
96
+ ## Example Script
97
+
98
+ *Bare minimum to run MuJoCo simulation and display result*
100
99
 
101
100
  ```python
102
101
  import mujoco_toolbox as mjtb
103
102
 
104
- # Optional Global Variables
105
- mjtb.VERBOSITY = True # Additional Performance and Debugging Print Statements (Default=False)
106
- mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
107
-
108
- # Example Code
109
- mjtb.Wrapper("path/to/your/xml").runSim(render=True).renderMedia()
103
+ mjtb.Wrapper("path/to/your/xml").run(render=True).save()
110
104
  ```
111
105
 
112
106
  ## Pre-Made Controllers
113
107
 
114
108
  ```python
115
- from mujoco_toolbox import \
116
- sineController,\
117
- cosineController,\
118
- stepController,\
119
- randomController,\
120
- realTimeController
109
+ from mujoco_toolbox.controller import (
110
+ cosine_controller,
111
+ random_controller,
112
+ live_controller,
113
+ sine_controller,
114
+ step_controller,
115
+ )
121
116
 
122
117
  # Wrapper can use custom controllers as well!
123
118
  ```
@@ -131,3 +126,4 @@ realTimeController
131
126
  ### URDF
132
127
 
133
128
  ![UR5](assets/images/ur5_render_no_gui.png)
129
+
@@ -2,6 +2,7 @@
2
2
  ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
3
3
  ![License](https://img.shields.io/github/license/MGross21/mujoco-toolbox)
4
4
  [![PyPI](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml)
5
+ [![Docs](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml)
5
6
 
6
7
  # Mujoco Toolbox
7
8
 
@@ -60,28 +61,26 @@ ffmpeg -version
60
61
 
61
62
  </details>
62
63
 
63
- ## To Run in Scripts
64
+ ## Example Script
65
+
66
+ *Bare minimum to run MuJoCo simulation and display result*
64
67
 
65
68
  ```python
66
69
  import mujoco_toolbox as mjtb
67
70
 
68
- # Optional Global Variables
69
- mjtb.VERBOSITY = True # Additional Performance and Debugging Print Statements (Default=False)
70
- mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
71
-
72
- # Example Code
73
- mjtb.Wrapper("path/to/your/xml").runSim(render=True).renderMedia()
71
+ mjtb.Wrapper("path/to/your/xml").run(render=True).save()
74
72
  ```
75
73
 
76
74
  ## Pre-Made Controllers
77
75
 
78
76
  ```python
79
- from mujoco_toolbox import \
80
- sineController,\
81
- cosineController,\
82
- stepController,\
83
- randomController,\
84
- realTimeController
77
+ from mujoco_toolbox.controller import (
78
+ cosine_controller,
79
+ random_controller,
80
+ live_controller,
81
+ sine_controller,
82
+ step_controller,
83
+ )
85
84
 
86
85
  # Wrapper can use custom controllers as well!
87
86
  ```
@@ -94,4 +93,4 @@ realTimeController
94
93
 
95
94
  ### URDF
96
95
 
97
- ![UR5](assets/images/ur5_render_no_gui.png)
96
+ ![UR5](assets/images/ur5_render_no_gui.png)
@@ -0,0 +1,74 @@
1
+ """Mujoco Toolbox.
2
+ ==============
3
+ A toolbox for working with MuJoCo simulations. This package provides various utilities and controllers to facilitate
4
+ the simulation process.
5
+
6
+ Modules:
7
+ --------
8
+ - `Wrapper`: Contains the Wrapper class for interfacing with MuJoCo.
9
+ - `Controller`: Includes sineController, cosineController, and randomController for controlling simulations.
10
+ - `Builder`: Contains the Builder class for creating and merging MuJoCo models.
11
+ - `assets`: Contains pre-defined assets for building MuJoCo models.
12
+
13
+ Constants:
14
+ ----------
15
+ - CAPTURE_PARAMETERS: List of MjData fields to capture during simulation.
16
+
17
+ Notes
18
+ -----
19
+ This package is still under development. Report any issues to https://github.com/MGross21/mujoco-toolbox/issues.
20
+
21
+ """
22
+
23
+ from .assets import WORLD_ASSETS, glovebox
24
+ from .builder import Builder
25
+ from .controller import (
26
+ cosine_controller,
27
+ live_controller,
28
+ random_controller,
29
+ sine_controller,
30
+ step_controller,
31
+ )
32
+ from .utils import _Platform, print_warning
33
+ from .wrapper import Wrapper
34
+
35
+ __version__ = "0.3.0"
36
+ __author__ = "Michael Gross"
37
+ __github_repo__ = "mujoco-toolbox"
38
+ __license__ = "MIT"
39
+ __status__ = "Development"
40
+ __all__ = [
41
+ "CAPTURE_PARAMETERS",
42
+ "COMPUTER",
43
+ "WORLD_ASSETS",
44
+ "Builder",
45
+ "Wrapper",
46
+ "cosine_controller",
47
+ "glovebox",
48
+ "live_controller",
49
+ "random_controller",
50
+ "sine_controller",
51
+ "step_controller",
52
+ ]
53
+
54
+ COMPUTER = _Platform() # Singleton Instance
55
+ MAX_GEOM_SCALAR: int = 2 # Scalar value for mujoco.Renderer.max_geom
56
+ CAPTURE_PARAMETERS = [ # MjData default fields to capture during simulation
57
+ "time",
58
+ "qpos",
59
+ "qvel",
60
+ "act",
61
+ "qacc",
62
+ "xpos",
63
+ "xquat",
64
+ "xmat",
65
+ "ctrl",
66
+ "sensordata",
67
+ ]
68
+
69
+ if __version__.startswith("0"):
70
+ print_warning(
71
+ f"{__package__} (v{__version__}) is still under development.",
72
+ f"Report any issues to https://github.com/MGross21/{__github_repo__}/issues",
73
+ )
74
+ del _Platform, print_warning
@@ -10,7 +10,7 @@ WORLD_ASSETS = """
10
10
  </asset>
11
11
  """
12
12
 
13
- def GloveBox(width=1.25, depth=0.75, height=1.0, glass_thickness=0.05):
13
+ def glovebox(width=1.25, depth=0.75, height=1.0, glass_thickness=0.05) -> str:
14
14
  return f"""
15
15
  <mujoco>
16
16
  <asset>
@@ -3,14 +3,30 @@ import xml.etree.ElementTree as ET
3
3
 
4
4
 
5
5
  class Builder:
6
- def __init__(self, xml_input: str):
7
- """ Initialize with an XML string or a file path. Input is required."""
8
- self.tree, self.root = self.load_model(xml_input)
6
+ def __init__(self, *args: str) -> None:
7
+ """Initialize with an XML string or a file path. Input is required."""
8
+ for arg in args:
9
+ if not isinstance(arg, str):
10
+ msg = "Input must be an XML string or a file path"
11
+ raise TypeError(msg)
12
+ if len(args) == 0:
13
+ msg = "Input is required to initialize the Builder"
14
+ raise ValueError(msg)
15
+ if len(args) > 1:
16
+ # Create a new Builder instance for each additional argument and merge them
17
+ self.tree, self.root = self.load_model(args[0])
18
+ for additional_arg in args[1:]:
19
+ additional_builder = Builder(additional_arg)
20
+ self.__add__(additional_builder)
21
+ else:
22
+ xml_input = args[0]
23
+ self.tree, self.root = self.load_model(xml_input)
9
24
 
10
25
  def load_model(self, xml_input):
11
- """ Load a MuJoCo model from a file or XML string."""
26
+ """Load a MuJoCo model from a file or XML string."""
12
27
  if not isinstance(xml_input, str):
13
- raise ValueError("Input must be either an XML string or a file path")
28
+ msg = "Input must be either an XML string or a file path"
29
+ raise ValueError(msg)
14
30
 
15
31
  # Check if input is an XML string
16
32
  if xml_input.strip().startswith("<"):
@@ -31,11 +47,13 @@ class Builder:
31
47
 
32
48
  return tree, tree.getroot()
33
49
  except ET.ParseError:
34
- raise ValueError("Invalid XML string provided")
50
+ msg = "Invalid XML string provided"
51
+ raise ValueError(msg)
35
52
  else:
36
53
  # Treat input as file path
37
54
  if not os.path.exists(xml_input):
38
- raise FileNotFoundError(f"File not found: {xml_input}")
55
+ msg = f"File not found: {xml_input}"
56
+ raise FileNotFoundError(msg)
39
57
 
40
58
  # Handle URDF files
41
59
  if xml_input.lower().endswith(".urdf"):
@@ -54,11 +72,11 @@ class Builder:
54
72
 
55
73
  return tree, tree.getroot()
56
74
  except ET.ParseError:
57
- raise ValueError(f"Invalid XML file: {xml_input}")
75
+ msg = f"Invalid XML file: {xml_input}"
76
+ raise ValueError(msg)
58
77
 
59
78
  def wrap_urdf_in_mujoco(self, urdf_input):
60
- """
61
- Wrap a URDF inside <mujoco> tags for MuJoCo compatibility.
79
+ """Wrap a URDF inside <mujoco> tags for MuJoCo compatibility.
62
80
  Input can be a file path, ElementTree, or XML string.
63
81
  """
64
82
  # Handle string input (could be a file path or XML string)
@@ -69,18 +87,21 @@ class Builder:
69
87
  root = ET.fromstring(urdf_input)
70
88
  tree = ET.ElementTree(root)
71
89
  except ET.ParseError:
72
- raise ValueError("Invalid URDF XML string provided")
90
+ msg = "Invalid URDF XML string provided"
91
+ raise ValueError(msg)
73
92
  else:
74
93
  # It's a file path
75
94
  try:
76
95
  tree = ET.parse(urdf_input)
77
96
  except:
78
- raise ValueError(f"Could not parse URDF file: {urdf_input}")
97
+ msg = f"Could not parse URDF file: {urdf_input}"
98
+ raise ValueError(msg)
79
99
  # Handle ElementTree input
80
100
  elif isinstance(urdf_input, ET.ElementTree):
81
101
  tree = urdf_input
82
102
  else:
83
- raise TypeError("URDF input must be a string or ElementTree")
103
+ msg = "URDF input must be a string or ElementTree"
104
+ raise TypeError(msg)
84
105
 
85
106
  root = tree.getroot()
86
107
 
@@ -95,8 +116,8 @@ class Builder:
95
116
  # Create a new tree with the <mujoco> root
96
117
  return ET.ElementTree(mujoco_elem), mujoco_elem
97
118
 
98
- def merge_tags(self, tag_name, root_1, root_2):
99
- """ Merge a specific tag (e.g., worldbody, asset) from two models."""
119
+ def merge_tags(self, tag_name, root_1, root_2) -> None:
120
+ """Merge a specific tag (e.g., worldbody, asset) from two models."""
100
121
  section_1 = root_1.find(tag_name)
101
122
  section_2 = root_2.find(tag_name)
102
123
 
@@ -111,7 +132,7 @@ class Builder:
111
132
  section_1.append(element)
112
133
 
113
134
  def __add__(self, other):
114
- """ Implement the + operator for merging two models."""
135
+ """Implement the + operator for merging two models."""
115
136
  model_root = None
116
137
 
117
138
  if isinstance(other, Builder):
@@ -121,7 +142,8 @@ class Builder:
121
142
  # If the input is a string, load as XML
122
143
  _, model_root = self.load_model(other)
123
144
  else:
124
- raise TypeError("Addition only supported between Builder objects or with XML strings/files")
145
+ msg = "Addition only supported between Builder objects or with XML strings/files"
146
+ raise TypeError(msg)
125
147
 
126
148
  # List of sections to merge
127
149
  sections_to_merge = ["asset", "worldbody", "camera", "light", "contact", "equality",
@@ -135,22 +157,24 @@ class Builder:
135
157
  return self
136
158
 
137
159
  def __radd__(self, other):
138
- """ Implement the reverse + operator for merging two models."""
160
+ """Implement the reverse + operator for merging two models."""
139
161
  if other == 0: # Handle sum() starting value
140
162
  return self
141
163
  return self.__add__(other)
142
164
 
143
- def save(self, file_path):
144
- """ Save the merged model to a file."""
165
+ def save(self, file_path) -> "os.path":
166
+ """Save the merged model to a file."""
145
167
  if self.tree is not None:
146
168
  # Format the XML with proper indentation before saving
147
169
  self._indent_xml(self.root)
148
170
  self.tree.write(file_path, encoding="utf-8", xml_declaration=True)
149
171
  else:
150
- raise ValueError("No model loaded. Cannot save.")
172
+ msg = "No model loaded. Cannot save."
173
+ raise ValueError(msg)
174
+ return os.path.abspath(file_path)
151
175
 
152
- def _indent_xml(self, elem, level=0):
153
- """ Add proper indentation to make the XML file more readable."""
176
+ def _indent_xml(self, elem, level=0) -> None:
177
+ """Add proper indentation to make the XML file more readable."""
154
178
  i = "\n" + level * " "
155
179
  if len(elem):
156
180
  if not elem.text or not elem.text.strip():
@@ -161,24 +185,27 @@ class Builder:
161
185
  self._indent_xml(sub_elem, level + 1)
162
186
  if not elem[-1].tail or not elem[-1].tail.strip():
163
187
  elem[-1].tail = i
164
- else:
165
- if level and (not elem.tail or not elem.tail.strip()):
166
- elem.tail = i
188
+ elif level and (not elem.tail or not elem.tail.strip()):
189
+ elem.tail = i
167
190
 
168
- def __str__(self):
169
- """ Return the XML string of the model."""
191
+ def __str__(self) -> str:
192
+ """Return the XML string of the model."""
170
193
  if self.tree is not None:
171
194
  # Format the XML with proper indentation
172
195
  self._indent_xml(self.root)
173
196
  return ET.tostring(self.root, encoding="unicode", method="xml")
174
- else:
175
- raise ValueError("No model loaded. Cannot generate string.")
197
+ msg = "No model loaded. Cannot generate string."
198
+ raise ValueError(msg)
176
199
 
177
- def __repr__(self):
178
- """ Return the XML string of the model."""
200
+ def __repr__(self) -> str:
201
+ """Return the XML string of the model."""
179
202
  return self.__str__()
180
203
 
204
+ def __len__(self) -> int:
205
+ """Return the number of elements in the model."""
206
+ return len(self.root) if self.root is not None else 0
207
+
181
208
  @property
182
209
  def xml(self):
183
- """ Property to quickly grab the XML string of the model."""
210
+ """Property to quickly grab the XML string of the model."""
184
211
  return str(self)
@@ -1,7 +1,7 @@
1
1
  from numpy import cos, pi, random, sin
2
2
 
3
3
 
4
- def _apply_control(model, data, value, joint=None, axis=None, delay=0):
4
+ def _apply_control(model, data, value, joint=None, axis=None, delay=0) -> None:
5
5
  """Common helper function for controller logic to reduce redundancy.
6
6
 
7
7
  Args:
@@ -11,6 +11,7 @@ def _apply_control(model, data, value, joint=None, axis=None, delay=0):
11
11
  joint (list[int], optional): Joints to apply control to.
12
12
  axis (int, optional): Axis to apply control to.
13
13
  delay (float, optional): Delay before applying control.
14
+
14
15
  """
15
16
  # Don't apply control until after delay
16
17
  if data.time < delay:
@@ -28,7 +29,7 @@ def _apply_control(model, data, value, joint=None, axis=None, delay=0):
28
29
  data.qpos[axis] = value
29
30
 
30
31
 
31
- def stepController(model, data, **kwargs):
32
+ def step_controller(model, data, **kwargs) -> None:
32
33
  """A step controller for the simulation.
33
34
 
34
35
  Args:
@@ -39,21 +40,24 @@ def stepController(model, data, **kwargs):
39
40
 
40
41
  Returns:
41
42
  None
43
+
42
44
  """
43
45
  amplitude = kwargs.get("amplitude", 1)
44
- joint = kwargs.get("joint", None)
45
- axis = kwargs.get("axis", None)
46
+ joint = kwargs.get("joint")
47
+ axis = kwargs.get("axis")
46
48
  delay = kwargs.get("delay", 0)
47
49
 
48
50
  if delay < 0:
49
- raise ValueError("Delay must be non-negative.")
51
+ msg = "Delay must be non-negative."
52
+ raise ValueError(msg)
50
53
  if joint is not None and axis is not None:
51
- raise ValueError("Cannot specify both 'joint' and 'axis'.")
54
+ msg = "Cannot specify both 'joint' and 'axis'."
55
+ raise ValueError(msg)
52
56
 
53
57
  _apply_control(model, data, amplitude, joint=joint, axis=axis, delay=delay)
54
58
 
55
59
 
56
- def sineController(model, data, **kwargs):
60
+ def sine_controller(model, data, **kwargs) -> None:
57
61
  """A simple sine wave controller for the simulation.
58
62
 
59
63
  Args:
@@ -65,21 +69,23 @@ def sineController(model, data, **kwargs):
65
69
 
66
70
  Returns:
67
71
  None
72
+
68
73
  """
69
74
  amplitude = kwargs.get("amplitude", 1)
70
75
  frequency = kwargs.get("frequency", 1)
71
76
  phase = kwargs.get("phase", 0)
72
- joint = kwargs.get("joint", None)
77
+ joint = kwargs.get("joint")
73
78
  delay = kwargs.get("delay", 0)
74
79
 
75
80
  if delay < 0:
76
- raise ValueError("Delay must be non-negative.")
81
+ msg = "Delay must be non-negative."
82
+ raise ValueError(msg)
77
83
 
78
84
  value = amplitude * sin(2 * pi * frequency * data.time + phase)
79
85
  _apply_control(model, data, value, joint=joint, delay=delay)
80
86
 
81
87
 
82
- def cosineController(model, data, **kwargs):
88
+ def cosine_controller(model, data, **kwargs) -> None:
83
89
  """A simple cosine wave controller for the simulation.
84
90
 
85
91
  Args:
@@ -91,21 +97,23 @@ def cosineController(model, data, **kwargs):
91
97
 
92
98
  Returns:
93
99
  None
100
+
94
101
  """
95
102
  amplitude = kwargs.get("amplitude", 1)
96
103
  frequency = kwargs.get("frequency", 1)
97
104
  phase = kwargs.get("phase", 0)
98
- joint = kwargs.get("joint", None)
105
+ joint = kwargs.get("joint")
99
106
  delay = kwargs.get("delay", 0)
100
107
 
101
108
  if delay < 0:
102
- raise ValueError("Delay must be non-negative.")
109
+ msg = "Delay must be non-negative."
110
+ raise ValueError(msg)
103
111
 
104
112
  value = amplitude * cos(2 * pi * frequency * data.time + phase)
105
113
  _apply_control(model, data, value, joint=joint, delay=delay)
106
114
 
107
115
 
108
- def randomController(model, data, **kwargs):
116
+ def random_controller(model, data, **kwargs) -> None:
109
117
  """A random controller for the simulation.
110
118
 
111
119
  Args:
@@ -116,21 +124,24 @@ def randomController(model, data, **kwargs):
116
124
 
117
125
  Returns:
118
126
  None
127
+
119
128
  """
120
129
  amplitude = kwargs.get("amplitude", 1)
121
- joint = kwargs.get("joint", None)
122
- axis = kwargs.get("axis", None)
130
+ joint = kwargs.get("joint")
131
+ axis = kwargs.get("axis")
123
132
  delay = kwargs.get("delay", 0)
124
133
 
125
134
  if delay < 0:
126
- raise ValueError("Delay must be non-negative.")
135
+ msg = "Delay must be non-negative."
136
+ raise ValueError(msg)
127
137
  if joint is not None and axis is not None:
128
- raise ValueError("Cannot specify both 'joint' and 'axis'.")
138
+ msg = "Cannot specify both 'joint' and 'axis'."
139
+ raise ValueError(msg)
129
140
 
130
141
  value = amplitude * random.rand()
131
142
  _apply_control(model, data, value, joint=joint, axis=axis, delay=delay)
132
143
 
133
- def realTimeController(model, data, **kwargs):
144
+ def live_controller(model, data, **kwargs) -> None:
134
145
  """A real-time controller for the simulation.
135
146
 
136
147
  Args:
@@ -138,8 +149,9 @@ def realTimeController(model, data, **kwargs):
138
149
 
139
150
  Returns:
140
151
  None
152
+
141
153
  """
142
- from .Utils import print_warning
154
+ from .utils import print_warning
143
155
  for key, value in kwargs.get("controller_params", {}).items():
144
156
  if hasattr(data, key):
145
157
  setattr(data, key, value)
@@ -150,4 +162,3 @@ def realTimeController(model, data, **kwargs):
150
162
  # if hasattr(data, 'control'):
151
163
  # data.control = kwargs.get("control", data.control)
152
164
 
153
- return
@@ -15,21 +15,17 @@ def timer(func):
15
15
  result = func(*args, **kwargs)
16
16
  end_time = time()
17
17
  total_time = end_time - start_time
18
-
19
- from . import VERBOSITY
20
-
21
- if VERBOSITY:
22
- print(f"Function '{func.__name__}' took {total_time:.4f} seconds")
18
+ print(f"Function {func.__name__} took {total_time:.4f} seconds to execute")
23
19
  return result
24
20
 
25
21
  return time_wrapper
26
22
 
27
23
  def print_warning(*args, prefix: bool = True) -> None:
28
- """
29
- Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
24
+ """Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
30
25
 
31
26
  Args:
32
27
  *args: First argument is printed as an orange warning, other arguments are printed normally.
28
+
33
29
  """
34
30
  if args:
35
31
  if prefix:
@@ -54,11 +50,11 @@ def print_warning(*args, prefix: bool = True) -> None:
54
50
  print(*args[1:])
55
51
 
56
52
  def print_success(*args, prefix: bool = True) -> None:
57
- """
58
- Prints the first argument as a green success message and prints the other arguments in regular text, all on the same line.
53
+ """Prints the first argument as a green success message and prints the other arguments in regular text, all on the same line.
59
54
 
60
55
  Args:
61
56
  *args: First argument is printed as a green success message, other arguments are printed normally.
57
+
62
58
  """
63
59
  if args:
64
60
  if prefix:
@@ -83,7 +79,7 @@ def print_success(*args, prefix: bool = True) -> None:
83
79
  print(*args[1:])
84
80
 
85
81
  class _Platform:
86
- def __init__(self):
82
+ def __init__(self) -> None:
87
83
  self.OS = platform.system()
88
84
  self.PROCESSOR = platform.processor()
89
85
  self.MACHINE = platform.machine()
@@ -95,25 +91,25 @@ class _Platform:
95
91
  self.RESOLUTION, self.GUI_ENABLED = self.get_resolution()
96
92
  # self.NUM_MONITORS = get_monitors().__len__()
97
93
 
98
- def __repr__(self):
94
+ def __repr__(self) -> str:
99
95
  return (
100
96
  f"Operating System: {self.OS}\n"
97
+ f"Architecture: {self.ARCHITECTURE}\n"
101
98
  f"Processor: {self.PROCESSOR}\n"
102
99
  f"Machine Type: {self.MACHINE}\n"
100
+ f"CPU Count: {self.CPU_COUNT}\n"
103
101
  f"Python Version: {self.PYTHON_VERSION}\n"
104
102
  f"IDE: {self.IDE}\n"
105
- f"CPU Count: {self.CPU_COUNT}\n"
106
- f"Architecture: {self.ARCHITECTURE}\n"
107
103
  f"Python Implementation: {self.PYTHON_IMPLEMENTATION}\n"
108
104
  f"Resolution (disp0): {self.RESOLUTION}\n"
109
105
  f"GUI Enabled: {self.GUI_ENABLED}"
110
106
  )
111
107
 
112
- def __str__(self):
108
+ def __str__(self) -> str:
113
109
  return self.__repr__()
114
110
 
115
111
  @staticmethod
116
- def get_developer_mode():
112
+ def get_developer_mode() -> str:
117
113
  if "ipykernel" in sys.modules and not sys.stdin.isatty():
118
114
  return "jupyter"
119
115
  return "terminal" if hasattr(sys, "ps1") else "script"