Repository: kubernetes-client/python (python)

Issue:
Exec provider exception
**What happened (please include outputs or screenshots)**:
Exception in **config/exec_provider.py:run** if the console is not attached and sys.stdout is None (for example if compile binary with pyinstaller) 
```
'NoneType' object has no attribute 'isatty'
```

**What you expected to happen**:
Exception is not happening

**How to reproduce it (as minimally and precisely as possible)**:
Use any config with exec, with any simple client, compile python to exe file with pyinstaller
```
apiVersion: v1
clusters:
- cluster:
    server: https://kube-oidc-proxy.lan
    insecure-skip-tls-verify: true
  name: default
contexts:
- context:
    cluster: default
    namespace: backend
    user: oidc
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: oidc
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - oidc-login
      - get-token
      - -v1
      - --oidc-issuer-url=https://some.lan/auth/realms/master
      - --oidc-client-id=kube-oidc-proxy
      - --oidc-client-secret=secret
      - --oidc-extra-scope=email
      - --grant-type=authcode
      - --authentication-timeout-sec=180
      command: kubectl
      env: null
      provideClusterInfo: false
```
This should call Popen inside exec_provider.py:run and open browser
```python
from kubernetes import client, config

# Configs can be set in Configuration class directly or using helper utility
config.load_kube_config()

v1 = client.CoreV1Api()
print("Listing pods with their IPs:")
ret = v1.list_pod_for_all_namespaces(watch=False)
for i in ret.items:
    print("%s\t%s\t%s" % (i.status.pod_ip, i.metadata.namespace, i.metadata.name))
```

**Anything else we need to know?**:
Suggested fix:
Changing this:
```python
    def run(self, previous_response=None):
        is_interactive = sys.stdout.isatty()
    ...
```
```python
    def run(self, previous_response=None):
       is_interactive = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
   ...
```
Also Popen opens a 'shim' console window, which can be hidden with something like:
```python
startupinfo = None
if platform.system().lower() == 'windows':
    startupinfo = subprocess.STARTUPINFO()
    startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
    startupinfo.wShowWindow = subprocess.SW_HIDE

process = subprocess.Popen(
    self.args,
    stdout=subprocess.PIPE,
    stderr=sys.stderr if is_interactive else subprocess.PIPE,
    stdin=sys.stdin if is_interactive else None,
    cwd=self.cwd,
    env=self.env,
    universal_newlines=True,
    startupinfo=startupinfo)
```
**Environment**:
- Kubernetes version: Client Version: v1.25.4, Kustomize Version: v4.5.7
- OS Windows: 11
- Python version: 3.10.9
- Python client version: 25.3.0


Pull request description:
Fix exception in ExecProvider when no console is attached.
#### What type of PR is this?

/kind bug

#### What this PR does / why we need it:

There are scenarios in python where sys.stdout can be None, for example when a python application is compiled with PyInstaller, or used in a TUI (text-based user-interface) application e.g. Textual.  This is mentioned in the python docs here:

https://docs.python.org/3/library/sys.html#sys.__stdout__

> Note Under some conditions stdin, stdout and stderr as well as the original values __stdin__, __stdout__ and __stderr__ can be None. It is usually the case for Windows GUI apps that aren’t connected to a console and Python apps started with pythonw.

This PR allows ExecProvider to work in such scenarios as these.  

#### Which issue(s) this PR fixes:

Fixes #1986 

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```NONE```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:


Patch:
diff --git a/kubernetes/base/config/exec_provider.py b/kubernetes/base/config/exec_provider.py
index 3f9c8ca0d..9dd382760 100644
--- a/kubernetes/base/config/exec_provider.py
+++ b/kubernetes/base/config/exec_provider.py
@@ -53,11 +53,11 @@ class ExecProvider(object):
                 value = item['value']
                 additional_vars[name] = value
             self.env.update(additional_vars)
-        
+
         self.cwd = cwd or None
 
     def run(self, previous_response=None):
-        is_interactive = sys.stdout.isatty()
+        is_interactive = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
         kubernetes_exec_info = {
             'apiVersion': self.api_version,
             'kind': 'ExecCredential',


Test patch:
diff --git a/kubernetes/base/config/exec_provider_test.py b/kubernetes/base/config/exec_provider_test.py
index b43926873..de8109499 100644
--- a/kubernetes/base/config/exec_provider_test.py
+++ b/kubernetes/base/config/exec_provider_test.py
@@ -149,6 +149,19 @@ class ExecProviderTest(unittest.TestCase):
         ep.run()
         self.assertEqual(mock.call_args[1]['cwd'], '/some/directory')
 
+    @mock.patch('subprocess.Popen')
+    def test_ok_no_console_attached(self, mock):
+        instance = mock.return_value
+        instance.wait.return_value = 0
+        instance.communicate.return_value = (self.output_ok, '')
+        mock_stdout = unittest.mock.patch(
+            'sys.stdout', new=None)  # Simulate detached console
+        with mock_stdout:
+            ep = ExecProvider(self.input_ok, None)
+            result = ep.run()
+            self.assertTrue(isinstance(result, dict))
+            self.assertTrue('token' in result)
+
 
 if __name__ == '__main__':
     unittest.main()