Repository: DetachHead/basedpyright (language: python)

Issue:
Inlay hints show ParamSpec as Callable
<!--
thanks for taking the time to raise an issue. please consider the following:

- if possible, create a minimal example in the playground: https://basedpyright.com
- does the issue also occur in pyright? (if you don't have pyright installed, its playground is available here: https://pyright-play.net)
  - note that the default rules are different in pyright (most checks are disabled by default). make sure to check the configuration options on the sidebar if your issue is with a specific diagnostic rule
  - if it does also occur in pyright, has it been raised upstream at https://github.com/microsoft/pyright? this isn't required, but it's useful to know if the problem has already been discussed. also if you're lucky and your issue doesn't get rejected, chances are it will be fixed faster.
- if you're reporting an issue with neovim, please provide step-by-step instructions to reproduce the issue including how you setup the editor. i have no idea how to use neovim so the more detailed your instructions the better
- if you're reporting an issue installing basedpyright, please mention what operating system and python version you're using
- if you're interested in fixing the issue yourself, we've tried our best to make the process to get set up for local development as easy as possible: https://docs.basedpyright.com/latest/development/contributing/#building
-->

`ParamSpec`s in inlay hints are shown `Callable[(args-and-kwargs...), Any]` while basedpyright says `(args-and-kwargs...)` correctly.

Code Example:

```python
from typing import Generic, ParamSpec, reveal_type

PS = ParamSpec("PS")


class A[**P]: ...

class C(Generic[PS]): ...

b = A()
m = C()

reveal_type(b)  # Type of "b" is "type[A[...]]"
reveal_type(m)  # Type of "m" is "type[C[...]]"
```

Screenshot:

![image](https://github.com/user-attachments/assets/1d229874-2560-4f39-8f39-37b1efdff0cd)



Discussion / hints:
Pylance also does have this problem when I tried to expose it.

Example code:

```python
from typing import Callable, reveal_type


class A[**P, R]:
    @classmethod
    def from_fn(cls, fn: Callable[P, R]) -> "A[P, R]": ...


def f(a: int) -> str: ...


b = A[[int], str].from_fn(f)

reveal_type(b)
```

Screenshot:

![image](https://github.com/user-attachments/assets/c71d6f0f-c10b-4249-b673-8fdbc48a7653)


Fix patch:
diff --git a/packages/pyright-internal/src/analyzer/typePrinter.ts b/packages/pyright-internal/src/analyzer/typePrinter.ts
index 6d924c6de..5f4da5f1b 100644
--- a/packages/pyright-internal/src/analyzer/typePrinter.ts
+++ b/packages/pyright-internal/src/analyzer/typePrinter.ts
@@ -845,6 +845,7 @@ function printFunctionType(
             );
         }
 
+        let result: string;
         if (isPositionalParamsOnly) {
             const paramTypes: string[] = [];
 
@@ -870,20 +871,19 @@ function printFunctionType(
 
             if (paramSpec) {
                 if (paramTypes.length > 0) {
-                    return `Callable[Concatenate[${paramTypes.join(', ')}, ${
-                        paramSpec.shared.name
-                    }], ${returnTypeString}]`;
+                    result = `Concatenate[${paramTypes.join(', ')}, ${paramSpec.shared.name}]`;
+                } else {
+                    result = paramSpec.shared.name;
                 }
-
-                return `Callable[${paramSpec.shared.name}, ${returnTypeString}]`;
+            } else {
+                result = `[${paramTypes.join(', ')}]`;
             }
-
-            return `Callable[[${paramTypes.join(', ')}], ${returnTypeString}]`;
         } else {
             // We can't represent this type using a Callable so default to
             // a "catch all" Callable.
-            return `Callable[..., ${returnTypeString}]`;
+            result = '...';
         }
+        return FunctionType.isParamSpecValue(type) ? result : `Callable[${result}, ${returnTypeString}]`;
     } else {
         const parts = printFunctionPartsInternal(
             type,


Test patch:
diff --git a/packages/pyright-internal/src/tests/typePrinter.test.ts b/packages/pyright-internal/src/tests/typePrinter.test.ts
index 00e235b89..94cb54860 100644
--- a/packages/pyright-internal/src/tests/typePrinter.test.ts
+++ b/packages/pyright-internal/src/tests/typePrinter.test.ts
@@ -186,3 +186,26 @@ test('FunctionTypes', () => {
     assert.strictEqual(printType(funcTypeD, PrintTypeFlags.None, returnTypeCallback), '(**P) -> Any');
     assert.strictEqual(printType(funcTypeD, PrintTypeFlags.PythonSyntax, returnTypeCallback), 'Callable[P, Any]');
 });
+
+describe('ParamSpec', () => {
+    test('positional only', () => {
+        const paramSpec = FunctionType.createSynthesizedInstance('', FunctionTypeFlags.ParamSpecValue);
+        for (const name of ['a', 'b']) {
+            FunctionType.addParam(
+                paramSpec,
+                FunctionParam.create(ParamCategory.Simple, AnyType.create(), FunctionParamFlags.TypeDeclared, name)
+            );
+        }
+        FunctionType.addPositionOnlyParamSeparator(paramSpec);
+        assert.strictEqual(printType(paramSpec, PrintTypeFlags.PythonSyntax, returnTypeCallback), '[Any, Any]');
+    });
+    test('keyword only', () => {
+        const paramSpec = FunctionType.createSynthesizedInstance('', FunctionTypeFlags.ParamSpecValue);
+        FunctionType.addKeywordOnlyParamSeparator(paramSpec);
+        FunctionType.addParam(
+            paramSpec,
+            FunctionParam.create(ParamCategory.Simple, AnyType.create(), FunctionParamFlags.TypeDeclared, 'a')
+        );
+        assert.strictEqual(printType(paramSpec, PrintTypeFlags.PythonSyntax, returnTypeCallback), '...');
+    });
+});