Repository: pyvisa/pyvisa

Issue:
[REQUEST] Support for integrating pyvisa-proxy
Hi Pyvisa Devs,

I am currently [developing an extension](https://github.com/casabre/pyvisa-proxy) for pyvisa which should somehow reflect the behavior of the NI remote VISA functionality but without the constraint of running NI-VISA. Due to the fact that there are many VISA flavors out there and NI-VISA is a bloated installer, I want to develop an independent and generic solution based on zmq which are connecting legacy protocols as GPIB or some USB connection. In the end, with mostly and ethernet-based instruments and pyvisa-proxy, distributed, dockerized tests will be feasible like for instance with my corresponding [Docker Image](https://hub.docker.com/r/casabre/rsvisa-python).

The architecture is not thought to then end but I just want to see if the PoC is working. However, after starting, I realized that implementing the client side is pretty hard because I want to forward the attribute calls to the server. Without relying on a specific class implementation, it was almost impossible to get the right attributes for forwarding.

Thus, I am asking if you could support me in order to integrate the logic into pyvisa with either a more monkey-patching approach of `__get_attr__` or an embedded implementation. 

Regarding the resource string, I have something similar in mind like the NI-VISA string &rarr; `proxy://hostname:port/remote resource`. After parsing, the pyvisa-proxy extension has to be loaded.

If that sounds interesting to you and I believe you have questions, please contact me.

Thanks & best regards
Carsten


Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 0d690f7..0b75ed0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,5 @@ htmlcov/
 .mypy_cache/
 pip-wheel-metadata/
 .pytest_cache
+.venv/
+venv/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0b11493..bc31bd6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,19 +1,18 @@
 repos:
-- repo: https://github.com/pre-commit/mirrors-isort
-  rev: v5.5.3
-  hooks:
-    - id: isort
-- repo: https://github.com/psf/black
-  rev: 20.8b1
-  hooks:
-    - id: black
-      language_version: python3.7
-- repo: https://gitlab.com/pycqa/flake8
-  rev: 3.8.3
-  hooks:
-    - id: flake8
-- repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v0.782  # Use the sha / tag you want to point at
-  hooks:
-    - id: mypy
-      additional_dependencies: [numpy, typing_extensions]
\ No newline at end of file
+  - repo: https://github.com/pre-commit/mirrors-isort
+    rev: v5.5.3
+    hooks:
+      - id: isort
+  - repo: https://github.com/psf/black
+    rev: 22.3.0
+    hooks:
+      - id: black
+  - repo: https://gitlab.com/pycqa/flake8
+    rev: 3.8.3
+    hooks:
+      - id: flake8
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v0.782 # Use the sha / tag you want to point at
+    hooks:
+      - id: mypy
+        additional_dependencies: [numpy, typing_extensions]
diff --git a/CHANGES b/CHANGES
index 7e2a919..d3bc016 100644
--- a/CHANGES
+++ b/CHANGES
@@ -15,6 +15,8 @@ PyVISA Changelog
 - allow trailing separators in ascii blocks PR #581
   Numpy style extraction already handled this case correctly but it was not so
   for the builtin parser.
+- adding open_resource attribute check for VisaLibraryBase in order to support
+  custom resource opening #660
 - changed constant ControlFlow enum type from IntEnum to IntFlag PR#652
   This change also triggers a change in attributes.py to include a new Attribute
   class, FlagAttribute where the enum type is declared at IntFlag. 
diff --git a/docs/source/advanced/backends.rst b/docs/source/advanced/backends.rst
index 4d60245..cb9a930 100644
--- a/docs/source/advanced/backends.rst
+++ b/docs/source/advanced/backends.rst
@@ -91,6 +91,12 @@ As a **very minimum** set you need:
 
 (you can get the signature below or here :ref:`api_visalibrarybase`)
 
+.. note:: 
+
+  A |ResourceManager| attribute check for 
+  :class:`pyvisa.highlevel.VisaLibraryBase.open_resource` is implemented 
+  in order to support custom opening handling by the new backend implementation.
+
 But of course you cannot do anything interesting with just this. In general you
 will also need:
 
diff --git a/pyvisa/constants.py b/pyvisa/constants.py
index da320a4..ecfeea0 100644
--- a/pyvisa/constants.py
+++ b/pyvisa/constants.py
@@ -18,7 +18,7 @@ import sys
 
 from typing_extensions import Literal
 
-is_64bits = sys.maxsize > 2 ** 32
+is_64bits = sys.maxsize > 2**32
 
 
 def _to_int(x: int) -> int:
diff --git a/pyvisa/highlevel.py b/pyvisa/highlevel.py
index af43216..720517e 100644
--- a/pyvisa/highlevel.py
+++ b/pyvisa/highlevel.py
@@ -3268,7 +3268,6 @@ class ResourceManager(object):
             Subclass of Resource matching the resource.
 
         """
-
         if resource_pyclass is None:
             info = self.resource_info(resource_name, extended=True)
 
@@ -3286,27 +3285,31 @@ class ResourceManager(object):
                     "There is no class defined for %r. Using Resource",
                     (info.interface_type, info.resource_class),
                 )
-
-        res = resource_pyclass(self, resource_name)
-        for key in kwargs.keys():
-            try:
-                getattr(res, key)
-                present = True
-            except AttributeError:
-                present = False
-            except errors.InvalidSession:
-                present = True
-
-            if not present:
-                raise ValueError(
-                    "%r is not a valid attribute for type %s"
-                    % (key, res.__class__.__name__)
-                )
-
-        res.open(access_mode, open_timeout)
-
-        for key, value in kwargs.items():
-            setattr(res, key, value)
+        if hasattr(self.visalib, "open_resource"):
+            res = self.visalib.open_resource(  # type: ignore
+                resource_name, access_mode, open_timeout, resource_pyclass, **kwargs
+            )
+        else:
+            res = resource_pyclass(self, resource_name)
+            for key in kwargs.keys():
+                try:
+                    getattr(res, key)
+                    present = True
+                except AttributeError:
+                    present = False
+                except errors.InvalidSession:
+                    present = True
+
+                if not present:
+                    raise ValueError(
+                        "%r is not a valid attribute for type %s"
+                        % (key, res.__class__.__name__)
+                    )
+
+            res.open(access_mode, open_timeout)
+
+            for key, value in kwargs.items():
+                setattr(res, key, value)
 
         self._created_resources.add(res)
 
diff --git a/pyvisa/shell.py b/pyvisa/shell.py
index 4712921..3e0d6d4 100644
--- a/pyvisa/shell.py
+++ b/pyvisa/shell.py
@@ -328,7 +328,7 @@ class VisaShell(cmd.Cmd):
 
         if not args:
             try:
-                charmap = {u"\r": "CR", u"\n": "LF", u"\r\n": "CRLF", u"\0": "NUL"}
+                charmap = {"\r": "CR", "\n": "LF", "\r\n": "CRLF", "\0": "NUL"}
                 chr = self.current.read_termination
                 if chr in charmap:
                     chr = charmap[chr]
@@ -349,10 +349,10 @@ class VisaShell(cmd.Cmd):
             )
         else:
             charmap = {
-                "CR": u"\r",
-                "LF": u"\n",
-                "CRLF": u"\r\n",
-                "NUL": u"\0",
+                "CR": "\r",
+                "LF": "\n",
+                "CRLF": "\r\n",
+                "NUL": "\0",
                 "None": None,
             }
             chr = args[0]


Test patch:
diff --git a/pyvisa/testsuite/fake-extensions/pyvisa_test_open.py b/pyvisa/testsuite/fake-extensions/pyvisa_test_open.py
new file mode 100644
index 0000000..6d010db
--- /dev/null
+++ b/pyvisa/testsuite/fake-extensions/pyvisa_test_open.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""
+
+"""
+from pyvisa import constants
+from pyvisa.highlevel import VisaLibraryBase
+from pyvisa.util import LibraryPath
+
+
+class FakeResource:
+    def close(self):
+        pass
+
+
+class FalseVISALib(VisaLibraryBase):
+    pass
+
+    @staticmethod
+    def get_library_paths():
+        return (LibraryPath("unset"),)
+
+    def _init(self):
+        pass
+
+    def open_resource(self, *args, **kwargs):
+        self.open_resource_called = True
+        return FakeResource()
+
+    def open_default_resource_manager(self):
+        return 1, constants.StatusCode.success
+
+    def close(self, session):
+        pass
+
+
+WRAPPER_CLASS = FalseVISALib
diff --git a/pyvisa/testsuite/test_highlevel.py b/pyvisa/testsuite/test_highlevel.py
index 4eb0d58..43974a3 100644
--- a/pyvisa/testsuite/test_highlevel.py
+++ b/pyvisa/testsuite/test_highlevel.py
@@ -5,10 +5,11 @@
 import logging
 import os
 import sys
+from importlib import import_module
 
 import pytest
 
-from pyvisa import constants, highlevel, resources, rname
+from pyvisa import ResourceManager, constants, highlevel, resources, rname
 from pyvisa.ctwrapper import IVIVisaLibrary
 
 from . import BaseTestCase
@@ -247,3 +248,22 @@ class TestHighlevel(BaseTestCase):
     def test_base_get_debug_info(self):
         """Test the base class implementation of get_debug_info."""
         assert len(highlevel.VisaLibraryBase.get_debug_info()) == 1
+
+    def test_open_resource_attr(self, caplog):
+        """Test handling errors when trying to open a Visa library."""
+        highlevel._WRAPPERS.clear()
+
+        path = os.path.join(os.path.dirname(__file__), "fake-extensions")
+        sys.path.append(path)
+        try:
+            pkg = import_module("pyvisa_test_open")
+            highlevel.get_wrapper_class("test_open")
+            rm = ResourceManager("@test_open")
+            assert rm is not None
+        finally:
+            sys.path.remove(path)
+
+        instr = rm.open_resource("TCPIP::192.168.0.1::INSTR")
+        assert isinstance(instr, pkg.FakeResource)
+        assert rm.visalib.open_resource_called
+        rm.close()