genie-python 15.1.0rc1__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.
Files changed (43) hide show
  1. genie_python/.pylintrc +539 -0
  2. genie_python/__init__.py +1 -0
  3. genie_python/block_names.py +123 -0
  4. genie_python/channel_access_exceptions.py +45 -0
  5. genie_python/genie.py +2462 -0
  6. genie_python/genie_advanced.py +418 -0
  7. genie_python/genie_alerts.py +195 -0
  8. genie_python/genie_api_setup.py +451 -0
  9. genie_python/genie_blockserver.py +64 -0
  10. genie_python/genie_cachannel_wrapper.py +545 -0
  11. genie_python/genie_change_cache.py +151 -0
  12. genie_python/genie_dae.py +2218 -0
  13. genie_python/genie_epics_api.py +906 -0
  14. genie_python/genie_experimental_data.py +186 -0
  15. genie_python/genie_logging.py +200 -0
  16. genie_python/genie_p4p_wrapper.py +203 -0
  17. genie_python/genie_plot.py +77 -0
  18. genie_python/genie_pre_post_cmd_manager.py +21 -0
  19. genie_python/genie_pv_connection_protocol.py +36 -0
  20. genie_python/genie_script_checker.py +507 -0
  21. genie_python/genie_script_generator.py +212 -0
  22. genie_python/genie_simulate.py +69 -0
  23. genie_python/genie_simulate_impl.py +1265 -0
  24. genie_python/genie_startup.py +29 -0
  25. genie_python/genie_toggle_settings.py +58 -0
  26. genie_python/genie_wait_for_move.py +154 -0
  27. genie_python/genie_waitfor.py +576 -0
  28. genie_python/matplotlib_backend/__init__.py +0 -0
  29. genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
  30. genie_python/mysql_abstraction_layer.py +272 -0
  31. genie_python/run_tests.py +56 -0
  32. genie_python/scanning_instrument_pylint_plugin.py +31 -0
  33. genie_python/typings/CaChannel/CaChannel.pyi +893 -0
  34. genie_python/typings/CaChannel/__init__.pyi +9 -0
  35. genie_python/typings/CaChannel/_version.pyi +6 -0
  36. genie_python/typings/CaChannel/ca.pyi +31 -0
  37. genie_python/utilities.py +406 -0
  38. genie_python/version.py +1 -0
  39. genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
  40. genie_python-15.1.0rc1.dist-info/METADATA +95 -0
  41. genie_python-15.1.0rc1.dist-info/RECORD +43 -0
  42. genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
  43. genie_python-15.1.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,36 @@
1
+ from collections.abc import Callable
2
+ from typing import TYPE_CHECKING, Protocol, Tuple, runtime_checkable
3
+
4
+ if TYPE_CHECKING:
5
+ from genie_python.genie import PVValue
6
+
7
+
8
+ @runtime_checkable
9
+ class GeniePvConnectionProtocol(Protocol):
10
+ @staticmethod
11
+ def set_pv_value(
12
+ name: str, value: "PVValue", wait: bool, timeout: float, safe_not_quick: bool
13
+ ) -> None: ...
14
+
15
+ @staticmethod
16
+ def clear_monitor(name: str, timeout: float) -> None: ...
17
+
18
+ @staticmethod
19
+ def get_pv_value(
20
+ name: str, to_string: bool, timeout: float, use_numpy: bool | None
21
+ ) -> "PVValue": ...
22
+
23
+ @staticmethod
24
+ def get_pv_timestamp(name: str, timeout: float) -> Tuple[int, int]: ...
25
+
26
+ @staticmethod
27
+ def pv_exists(name: str, timeout: float) -> bool: ...
28
+
29
+ @staticmethod
30
+ def add_monitor(
31
+ name: str,
32
+ call_back_function: "Callable[[PVValue, str, str], None]",
33
+ link_alarm_on_disconnect: bool = True,
34
+ to_string: bool = False,
35
+ use_numpy: bool | None = None,
36
+ ) -> Callable[[], None]: ...
@@ -0,0 +1,507 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import ast
4
+ import json
5
+ import os
6
+ import re
7
+ import site
8
+ import subprocess
9
+ import sys
10
+ import sysconfig
11
+ import unicodedata
12
+ from builtins import object
13
+ from io import StringIO, open
14
+ from typing import Iterable
15
+
16
+ from astroid import MANAGER, nodes
17
+ from pylint.lint import Run
18
+ from pylint.reporters.text import TextReporter
19
+
20
+
21
+ class ScriptChecker(object):
22
+ """
23
+ Check Scripts for common errors
24
+ """
25
+
26
+ def _find_regex(self, variable: str) -> str:
27
+ """
28
+ Sets the function to find any of the symbols listed below
29
+ Args:
30
+ variable: the assigned string from the search function
31
+ Return:
32
+ the string to be used in the regex search function
33
+ """
34
+ assignment_regex = "[\|\&\^\/\+\-\*\%]?=[^=]"
35
+ regex = r"\b{0}[.][\w\s]*" + assignment_regex + r"|\b{0}[\s]*" + assignment_regex
36
+ return regex.format(variable)
37
+
38
+ def _check_g_inst_name(self, line: str, line_no: int) -> str:
39
+ """
40
+ Checks a line of a script for assignments of variables named g or inst
41
+ Args:
42
+ line: the line to check
43
+ line_no: the line number
44
+ Return:
45
+ If an error is found appropriate warning string else
46
+ if no error found an empty string
47
+ """
48
+ g_error = re.search(self._find_regex("g"), line)
49
+ if g_error:
50
+ return "W: {line_no}: 'g' assignment in line {line_no}".format(line_no=line_no)
51
+
52
+ inst_error = re.search(self._find_regex("inst"), line)
53
+ if inst_error:
54
+ return "W: {line_no}: 'inst' assignment in line {line_no}".format(line_no=line_no)
55
+
56
+ return ""
57
+
58
+ def check_script_lines(self, lines: Iterable[str]) -> list[str]:
59
+ """
60
+ Check the lines of the script for possible errors
61
+ Args:
62
+ lines: iterable of lines to check
63
+ Returns: error in the script; empty list if none
64
+ """
65
+ reassignment_warnings = []
66
+ line_no = 0
67
+ for line in lines:
68
+ line_no += 1
69
+ warning = self._check_g_inst_name(line, line_no)
70
+ if len(warning) != 0:
71
+ reassignment_warnings.append(warning)
72
+
73
+ return reassignment_warnings
74
+
75
+ def _can_cache_module(self, module_name: str, module: nodes.Module) -> bool:
76
+ """
77
+ Determines whether a module can be cached or whether the linter
78
+ should re-examine it's contents each time.
79
+
80
+ Args:
81
+ module_name: the name of the module
82
+ module: the astroid module object
83
+
84
+ Returns:
85
+ True if the module can safely be cached, False otherwise
86
+ """
87
+ # Always allow builtin modules to be cached.
88
+ if module_name in sys.builtin_module_names:
89
+ return True
90
+
91
+ # Allow modules defined in the site packages directories to be cached as
92
+ # they are unlikely to change at runtime
93
+ if module.file is not None and any(
94
+ module.file.startswith(site_package_dir) for site_package_dir in site.getsitepackages()
95
+ ):
96
+ return True
97
+
98
+ # Other modules are probably user-defined e.g. inst scripts,
99
+ # shared scripts, user scripts. Don't cache these.
100
+ return False
101
+
102
+ def _clean_astroid_cache(self) -> None:
103
+ """
104
+ Cleans user-defined scripts out of the astroid cache.
105
+ """
106
+ new_cache = {}
107
+
108
+ for module_name, module in MANAGER.astroid_cache.items():
109
+ if self._can_cache_module(module_name, module):
110
+ new_cache[module_name] = module
111
+
112
+ MANAGER.astroid_cache = new_cache
113
+
114
+ class _TemporaryPyrightConfig:
115
+ def __init__(
116
+ self,
117
+ config_path: str,
118
+ instrument_name: str,
119
+ additional_include_paths: list[str] | None = None,
120
+ ) -> None:
121
+ if additional_include_paths is None:
122
+ additional_include_paths = []
123
+
124
+ self.config_path = config_path
125
+ self.config_name = "pyrightconfig.json"
126
+ self.json_write = {
127
+ "include": [
128
+ ".",
129
+ ],
130
+ "extraPaths": [
131
+ os.path.join(sysconfig.get_paths()["purelib"]),
132
+ os.path.join(sysconfig.get_paths()["platlib"]),
133
+ os.path.join("C:\\", "Instrument", "scripts"),
134
+ os.path.join(
135
+ "C:\\", "Instrument", "settings", "config", instrument_name, "Python"
136
+ ),
137
+ os.path.join("U:\\", "scripts"),
138
+ os.path.join("U:\\"),
139
+ ],
140
+ "exclude": [
141
+ "**/node_modules",
142
+ "**/__pycache__",
143
+ ],
144
+ "typeCheckingMode": "basic",
145
+ "reportUnusedVariable": False,
146
+ "reportOptionalMemberAccess": False,
147
+ "reportOptionalSubscript": False,
148
+ "reportOptionalCall": False,
149
+ "reportOptionalIterable": False,
150
+ "reportUnboundVariable": False,
151
+ "reportUndefinedVariable ": False,
152
+ # Errors such as these will be caught by pylint before pyright, so no need to report
153
+ "reportMissingImports": False,
154
+ # Errors such as these will be caught by pylint before pyright, so no need to report
155
+ "strictParameterNoneValue": False,
156
+ "reportOptionalOperand": False,
157
+ "pythonPlatform": "Windows",
158
+ }
159
+
160
+ for path in additional_include_paths:
161
+ self.json_write["extraPaths"].append(path)
162
+
163
+ def __enter__(self) -> None:
164
+ self._filename = os.path.join(self.config_path, self.config_name)
165
+ with open(self._filename, "w") as f:
166
+ f.write(json.dumps(self.json_write))
167
+
168
+ def __exit__(self, exc_type: None, exc_value: None, exc_traceback: None) -> None:
169
+ os.unlink(self._filename)
170
+
171
+ def pyright_script_checker(
172
+ self, script_path: str, instrument_name: str, pyright_additional_include: list[str]
173
+ ) -> tuple[list[str], list[str]]:
174
+ """
175
+ Makes a call to pyright to do a static analyis of the script.
176
+
177
+ Args:
178
+ script_name: The path to the selected user script
179
+ instrument_name: The instrument name in
180
+ C:\\Instrument\\Settings\\config\\[instrument_name]\\Python
181
+ Returns:
182
+ A list of warnings and a list of errors.
183
+ """
184
+
185
+ script_dir = os.path.dirname(script_path)
186
+ errors = []
187
+ warnings = []
188
+
189
+ # 1 - Checks to see if pyrightconfig.json is present
190
+ # under the same directory as the selected script
191
+ # 2 - if not then copy json into dir
192
+ # 3 - Run pyright --project C:/[path_to_script_dir] C:/[path_to_script_dir]/[script].py
193
+ # 4 - reads from json output and returns appropriate errors/warnings/nothing if OK
194
+
195
+ with self._TemporaryPyrightConfig(script_dir, instrument_name, pyright_additional_include):
196
+ cmd = [
197
+ sys.executable,
198
+ "-m",
199
+ "pyright",
200
+ "--project",
201
+ script_dir,
202
+ "--outputjson",
203
+ script_path,
204
+ ]
205
+
206
+ pr_result = subprocess.run(args=cmd, capture_output=True, text=True, encoding="utf-8")
207
+ json_out = unicodedata.normalize("NFKD", pr_result.stdout)
208
+
209
+ json_data = json.loads(json_out)
210
+
211
+ # for each diagnostic, if severity is error then
212
+ # add message to error array else add to warning array
213
+ for diagnostic in json_data["generalDiagnostics"]:
214
+ start = diagnostic["range"]["start"]
215
+ if not diagnostic["rule"] == "reportUndefinedVariable": ### CHANGE
216
+ if diagnostic["severity"] == "error":
217
+ errors += [
218
+ f"E: {start['line'] + 1}: "
219
+ f"{diagnostic['message']} [{diagnostic['rule']}]"
220
+ ]
221
+ else:
222
+ warnings += [
223
+ f"W: {start['line'] + 1}: "
224
+ f"{diagnostic['message']} [{diagnostic['rule']}]"
225
+ ]
226
+
227
+ return warnings, errors
228
+
229
+ def check_for_tabs(
230
+ self,
231
+ lines_list: list[str],
232
+ error_line_numbers: list[int],
233
+ lines_containing_errors: list[str],
234
+ ) -> None:
235
+ """
236
+ Searches for tabs in script file. Tells user to convert tabs to 4 space characters.
237
+
238
+ Args:
239
+ lines_list: List of all lines in script.
240
+ error_line_numbers: List of line numbers which have errors.
241
+ lines_containing_errors: List of lines that contain errors.
242
+ """
243
+
244
+ tab_found = False
245
+ for i, line in enumerate(lines_list, start=1):
246
+ if i in error_line_numbers:
247
+ lines_containing_errors.append(line.strip())
248
+
249
+ tab_line = re.search(r"\t", line)
250
+ if tab_line:
251
+ tab_found = True
252
+
253
+ if tab_found:
254
+ print(
255
+ "Tab characters found in file, for portability convert tabs to 4 space characters."
256
+ )
257
+
258
+ def check_script(
259
+ self,
260
+ script_name: str,
261
+ instrument_name: str,
262
+ warnings_as_error: bool = False,
263
+ no_pyright: bool = False,
264
+ no_pylint: bool = False,
265
+ pyright_additional_include: list[str] | None = None,
266
+ ) -> list[str]:
267
+ """
268
+ Check a script for common errors.
269
+ Args:
270
+ script_name: filename of the script
271
+ instrument_name: Full instrument name
272
+ warnings_as_error: True treat warnings as errors; False otherwise
273
+
274
+ Returns: error messages list; empty list if there are no errors
275
+ """
276
+ errors_output = StringIO()
277
+
278
+ # We need to clean the cache so that we pick up changes in instrument scripts
279
+ self._clean_astroid_cache()
280
+
281
+ warnings = []
282
+ errors = []
283
+
284
+ dir_path = os.path.dirname(os.path.realpath(__file__))
285
+
286
+ if no_pylint and no_pyright:
287
+ return []
288
+
289
+ elif not no_pylint:
290
+ pylint_path = os.path.join(dir_path, ".pylintrc")
291
+
292
+ with open(script_name) as f:
293
+ reassignment_warnings = self.check_script_lines(f)
294
+ warnings.extend(reassignment_warnings)
295
+
296
+ inst_file_path = os.path.join(
297
+ "C:\\", "Instrument", "Settings", "config", instrument_name, "Python"
298
+ )
299
+ init_hook = (
300
+ "import sys;"
301
+ 'sys.path.append("{}");'
302
+ 'sys.path.append("C:\\Instrument\\scripts");'.format(inst_file_path)
303
+ )
304
+ init_hook = init_hook.replace("\\", "\\\\")
305
+ inst_scripts_file_path = os.path.join(inst_file_path, "inst")
306
+
307
+ try:
308
+ functions = self.get_inst_attributes(inst_scripts_file_path)
309
+ except Exception as e:
310
+ match = re.search(r"\((.*?),", str(e))
311
+ assert match is not None, "Regex searching for expression failed."
312
+ e_filename = match.group(1)
313
+ return [
314
+ "Error while getting attributes of instrument scripts. Please check "
315
+ + os.path.join(inst_scripts_file_path, e_filename)
316
+ + ": {}".format(e)
317
+ ]
318
+
319
+ # C = Convention related checks,
320
+ # R = Refactoring Related Checks,
321
+ # W = various warnings,
322
+ # E = Errors,
323
+ # F = fatal
324
+ # --msg-template={msg_id}:{line:3d},{column}: {obj}: {msg} for more specific message
325
+ Run(
326
+ [
327
+ "--rcfile={}".format(pylint_path),
328
+ "--init-hook={}".format(init_hook),
329
+ "--msg-template={C}:{line:3d}: {msg} ({symbol})",
330
+ "--generated-members={}".format(functions),
331
+ "--score=n",
332
+ script_name,
333
+ ],
334
+ reporter=TextReporter(errors_output),
335
+ exit=False,
336
+ )
337
+
338
+ new_warnings, errors = self.split_warning_errors(errors_output)
339
+ warnings += new_warnings
340
+
341
+ if errors == [] and not no_pyright: # Don't run pryight if pylint goes wrong
342
+ pyright_warnings, pyright_errors = self.pyright_script_checker(
343
+ script_name, instrument_name, pyright_additional_include or []
344
+ )
345
+
346
+ errors += pyright_errors
347
+ warnings += pyright_warnings
348
+
349
+ error_line_numbers = []
350
+
351
+ if warnings_as_error:
352
+ errors += warnings
353
+ else:
354
+ for warning in warnings:
355
+ print(warning)
356
+
357
+ error_line = re.search(r"W:\s+(\d+):", warning)
358
+ if error_line:
359
+ selected_number = error_line.group(1)
360
+
361
+ error_line_numbers.append(int(selected_number))
362
+
363
+ lines_containing_errors = []
364
+ with open(script_name) as f:
365
+ content = f.read()
366
+
367
+ lines_list = content.splitlines()
368
+
369
+ self.check_for_tabs(lines_list, error_line_numbers, lines_containing_errors)
370
+
371
+ line_numbers_position = 0
372
+
373
+ for line in lines_containing_errors:
374
+ g_error_line = re.search(r"g\.", line)
375
+ if g_error_line:
376
+ errors.append(
377
+ f"An error has been found on line {error_line_numbers[line_numbers_position]} "
378
+ f"within the file {script_name}"
379
+ )
380
+ errors.append(
381
+ 'Please check the "g." statement and ensure that the brackets are not missing.'
382
+ )
383
+
384
+ return errors
385
+
386
+ def split_warning_errors(self, errors_outputs: Iterable[str]) -> tuple[list[str], list[str]]:
387
+ """
388
+ takes in errors and warning lists and split in two separate list i.e.
389
+ (errors and warnings)
390
+ :param errors_outputs: list of errors and warnings
391
+ :return: two separate lists for errors and warnings
392
+ """
393
+ warnings = []
394
+ errors = []
395
+ errors_outputs = errors_outputs.getvalue().split("\n")
396
+ verbose_warning = [
397
+ "Redefining name 'g' from outer scope",
398
+ "Redefining name 'inst' from outer scope",
399
+ ]
400
+ verbose_warning = [
401
+ error
402
+ for error in errors_outputs
403
+ if any(warning in error for warning in verbose_warning)
404
+ ]
405
+
406
+ for message in errors_outputs:
407
+ if message.startswith("W") and (message not in verbose_warning):
408
+ warnings.append(message)
409
+ elif message.startswith("E"):
410
+ errors.append(message)
411
+
412
+ return warnings, errors
413
+
414
+ def get_inst_attributes(self, instrument_scripts_paths: str) -> str | list[str]:
415
+ """
416
+ gets attributes such as Global variables, Functions, Classes defined
417
+ in instrument scripts
418
+ :param instrument_scripts_paths: path to instrument scripts
419
+ :return: string representation of attributes present in
420
+ instrument scripts with comma separated
421
+ """
422
+ try:
423
+ attributes = ""
424
+ for filename in os.listdir(instrument_scripts_paths):
425
+ if filename.endswith(".py") and not filename.startswith("__"):
426
+ with open(os.path.join(instrument_scripts_paths, filename)) as f:
427
+ src = f.read()
428
+ tree = ast.parse(src, filename)
429
+ attributes += self.get_all_attributes(tree)
430
+ attributes = attributes[:-1]
431
+ return attributes
432
+ except OSError:
433
+ return ""
434
+
435
+ def get_all_attributes(self, tree: nodes.Module) -> str | list[str]:
436
+ """
437
+ gets all the attributes of instrument scripts
438
+ :param tree: abstract syntax tree representation of instrument script
439
+ :return: string of all the useful attributes
440
+ """
441
+ attributes = self.get_names_of_functions_classes_global_variables(tree.body)
442
+ return attributes
443
+
444
+ def get_names_of_functions_classes_global_variables(self, body: Iterable) -> str | list[str]:
445
+ """
446
+ gets the name of function, class and global variable names
447
+ :param body: body to iterate through
448
+ :return: string of function names, class, global variables (comma separated)
449
+ """
450
+ attributes = ""
451
+ for item in body:
452
+ # getting functions in global scope
453
+ if isinstance(item, ast.FunctionDef) and not item.name.startswith("_"):
454
+ attributes += "inst.{function_name},".format(function_name=item.name)
455
+ # getting class and its attributes
456
+ elif isinstance(item, ast.ClassDef):
457
+ class_name = "inst.{class_name}".format(class_name=item.name)
458
+ attributes += "{class_name},".format(class_name=class_name)
459
+ attributes += self.get_class_member_names(item.body, class_name)
460
+ elif isinstance(item, ast.Assign):
461
+ for target in item.targets:
462
+ attributes += self.parse_assignment_target(target)
463
+
464
+ return attributes
465
+
466
+ def parse_assignment_target(self, target: ast.AST, descendants: str = "") -> list[str]:
467
+ if isinstance(target, ast.Name):
468
+ return "inst.{}{},".format(target.id, descendants)
469
+
470
+ if sys.version_info[0] >= 3 and isinstance(target, ast.Starred):
471
+ return self.parse_assignment_target(target.value, descendants=descendants)
472
+
473
+ if isinstance(target, ast.Attribute):
474
+ descendants = ".{}{}".format(target.attr, descendants)
475
+ return self.parse_assignment_target(target.value, descendants=descendants)
476
+
477
+ if isinstance(target, (ast.List, ast.Tuple)):
478
+ attributes = ""
479
+ for nested_target in target.elts:
480
+ attributes += self.parse_assignment_target(nested_target)
481
+ return attributes
482
+
483
+ # Ignore all other nodes
484
+ return ""
485
+
486
+ def get_class_member_names(self, body: Iterable, class_name: str) -> list[str]:
487
+ """
488
+ gets the name of all the class members
489
+ :param body: body to iterate through
490
+ :param class_name: name of class to prepend
491
+ :return: string of class member names (comma separated)
492
+ """
493
+ attributes = ""
494
+ for function_body in body:
495
+ if isinstance(function_body, ast.FunctionDef):
496
+ if "__" not in function_body.name:
497
+ attributes += "{class_name}.{function_name},".format(
498
+ class_name=class_name, function_name=function_body.name
499
+ )
500
+ else:
501
+ # variables defined inside __init__
502
+ for variables in function_body.body:
503
+ if isinstance(variables, ast.Assign):
504
+ attributes += "{class_name}.{variable_name},".format(
505
+ class_name=class_name, variable_name=variables.targets[0].attr
506
+ )
507
+ return attributes