Repository: pyvisa/pyvisa

Issue:
Add support for custom simulation files to pyvisa-shell
Running `pyvisa-shell --backend sim` does not appear to support custom simulation files.

The fix looks very easy - I'll submit a PR.


### Steps to Reproduce:

1. Create the sim file shown below.
2. Run `pyvisa-shell --backend pyvisa_test.yaml@sim`
3. See Errror


### Expected Result

The custom simulation file is used


### Actual result

The program errors out.

```console
$ pyvisa-shell --backend pyvisa_test.yaml@sim
Traceback (most recent call last):
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/ctwrapper/highlevel.py", line 162, in _init
    lib = Library(self.library_path)
  File "/opt/python3.10.10/lib/python3.10/ctypes/__init__.py", line 374, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: @pyvisa_test.yaml@sim: cannot open shared object file: No such file or directory

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/bin/pyvisa-shell", line 8, in <module>
    sys.exit(visa_shell())
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/cmd_line_tools.py", line 62, in visa_shell
    visa_main("shell")
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/cmd_line_tools.py", line 53, in visa_main
    shell.main("@" + args.backend if args.backend else "")
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/shell.py", line 387, in main
    VisaShell(library_path).cmdloop()
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/shell.py", line 30, in __init__
    self.resource_manager = ResourceManager(library_path)
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/highlevel.py", line 2992, in __new__
    visa_library = open_visa_library(visa_library)
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/highlevel.py", line 2904, in open_visa_library
    return cls(argument)
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/highlevel.py", line 191, in __new__
    obj._init()
  File "/usr/local/google/home/dthor/.virtualenvs/pyle/lib/python3.10/site-packages/pyvisa/ctwrapper/highlevel.py", line 164, in _init
    raise errors.LibraryError.from_exception(exc, self.library_path)
pyvisa.errors.LibraryError: Error while accessing @pyvisa_test.yaml@sim: @pyvisa_test.yaml@sim: cannot open shared object file: No such file or directory
```


### Sim file

```yaml
# pyvisa_test.yaml    
spec: "1.0"                                    
devices:               
  device 1:
    eom:              
      TCPIP SOCKET:
        q: "\n"
        r: "\n"
    error: ERROR                           
    dialogues:                 
      - q: "*IDN?"
        r: "Fake Instr"
                                                           
resources:                                                
  TCPIP0::1.2.3.4::999::SOCKET:                         
    device: device 1
```

---

**Output of `pyvisa-info`**

```console
$ pyvisa-info
Machine Details:
   Platform ID:    Linux-6.3.11-1rodete2-amd64-x86_64-with-glibc2.37
   Processor:      

Python:
   Implementation: CPython
   Executable:     /usr/local/google/home/dthor/.virtualenvs/pyle/bin/python3
   Version:        3.10.10
   Compiler:       GCC 10.2.1 20210110
   Bits:           64bit
   Build:          Mar  7 2023 16:45:52 (#main)
   Unicode:        UCS4

PyVISA Version: 1.13.0

Backends:
   ivi:
      Version: 1.13.0 (bundled with PyVISA)
      Binary library: Not found
   py:
      Version: 0.6.2
      ASRL INSTR: Available via PySerial (3.5)
      USB INSTR: Available via PyUSB (1.0.2). Backend: N/A
      USB RAW: Available via PyUSB (1.0.2). Backend: N/A
      TCPIP INSTR: Available 
         Resource discovery:
         - VXI-11: ok
         - hislip: ok
      VICP INSTR:
         Please install PyVICP to use this resource type.
      TCPIP SOCKET: Available 
      GPIB INSTR:
         Please install linux-gpib (Linux) or gpib-ctypes (Windows, Linux) to use this resource type. Note that installing gpib-ctypes will give you access to a broader range of funcionality.
         No module named 'gpib'
   sim:
      Version: 0.5.1
      Spec version: 1.1
```


Discussion:
MatthieuDartiailh: Can you fix the linter issues ?
MatthieuDartiailh: I will try to fix the mypy issues and review this next.
MatthieuDartiailh: Can you rebase on main now that the mypy issues are fixed ?

Patch:
diff --git a/CHANGES b/CHANGES
index c9976ff..02157de 100644
--- a/CHANGES
+++ b/CHANGES
@@ -15,6 +15,8 @@ PyVISA Changelog
   - `p.u.get_shared_library_arch` now returns `p.u.PEMachineType` instead of a string
   - `p.u.get_arch` now returns a list of `p.u.ArchitectureType`
 - update `Resource` context manager type annotation to preserve derived type PR # 740
+- added support for using custom simulation files when using the `sim` backend
+  with `pyvisa-shell`. PR #774
 
 1.13.0 (22-12-2022)
 -------------------
diff --git a/docs/source/introduction/shell.rst b/docs/source/introduction/shell.rst
index cc4d7ed..5cd0662 100644
--- a/docs/source/introduction/shell.rst
+++ b/docs/source/introduction/shell.rst
@@ -158,6 +158,10 @@ You can invoke::
 to use python-sim as backend instead of ni backend.
 This can be used for example for testing of python-sim configuration.
 
+You can include a specific file to use for the simulated backend::
+
+    pyvisa-shell -b file.yaml@sim
+
 You can invoke::
 
     pyvisa-shell -b py
diff --git a/pyvisa/cmd_line_tools.py b/pyvisa/cmd_line_tools.py
index f8218a4..a31f23b 100644
--- a/pyvisa/cmd_line_tools.py
+++ b/pyvisa/cmd_line_tools.py
@@ -7,6 +7,7 @@ This file is part of PyVISA.
 :license: MIT, see LICENSE for more details.
 
 """
+import argparse
 from typing import Optional
 
 
@@ -50,13 +51,27 @@ def visa_main(command: Optional[str] = None) -> None:
     elif args.command == "shell":
         from pyvisa import shell
 
-        shell.main("@" + args.backend if args.backend else "")
+        backend = _create_backend_str(args)
+        shell.main(backend)
     else:
         raise ValueError(
             f"Unknown command {args.command}. Valid values are: info and shell"
         )
 
 
+def _create_backend_str(args: argparse.Namespace) -> str:
+    """Create the backend string from the CLI arguments."""
+    if not args.backend:
+        return ""
+
+    # User already entered things correctly like "@py" or "file.yaml@sim"
+    if "@" in args.backend:
+        return args.backend
+
+    # Keep the current functionality
+    return "@" + args.backend
+
+
 def visa_shell() -> None:
     """Run the VISA shell CLI program."""
     visa_main("shell")


Test patch:
diff --git a/pyvisa/testsuite/test_cmd_line_tools.py b/pyvisa/testsuite/test_cmd_line_tools.py
index 349da30..8e490a4 100644
--- a/pyvisa/testsuite/test_cmd_line_tools.py
+++ b/pyvisa/testsuite/test_cmd_line_tools.py
@@ -2,12 +2,13 @@
 """Test the behavior of the command line tools.
 
 """
+import argparse
 import sys
 from subprocess import PIPE, Popen, run
 
 import pytest
 
-from pyvisa import util
+from pyvisa import cmd_line_tools, util
 
 from . import BaseTestCase, require_visa_lib
 
@@ -41,3 +42,18 @@ class TestCmdLineTools(BaseTestCase):
         with Popen(["pyvisa-shell"], stdin=PIPE, stdout=PIPE) as p:
             stdout, stderr = p.communicate(b"exit")
         assert stdout.count(b"Welcome to the VISA shell") == 1
+
+
+@pytest.mark.parametrize(
+    "args, want",
+    [
+        (argparse.Namespace(backend=None), ""),
+        (argparse.Namespace(backend="py"), "@py"),
+        (argparse.Namespace(backend="foo.yaml@sim"), "foo.yaml@sim"),
+        (argparse.Namespace(backend="/foo/bar/baz.yaml@sim"), "/foo/bar/baz.yaml@sim"),
+        (argparse.Namespace(backend="@sim"), "@sim"),
+    ],
+)
+def test__create_backend_str(args: argparse.Namespace, want: str) -> None:
+    got = cmd_line_tools._create_backend_str(args)
+    assert got == want