Repository: apiflask/apiflask (python)

Issue:
KeyError when using add_url_for with flask View 
## context

[flask-graphql](https://github.com/graphql-python/flask-graphql) is a project that adds graphql support to flask applications. 

apiflask produces an error when using `add_url_for` with view_func=`GraphQLView.as_view(...)`.

## steps to reproduce 

1. setup a minimal apiflask application that uses flask-graphql
```
#requirements.txt 
apiflask 
flask-graphql
``` 
```python
# app.py
from apiflask import APIFlask
from flask_graphql import GraphQLView
from graphql.type.schema import GraphQLSchema
from graphql.type.scalars import GraphQLString
from graphql.type.definition import (GraphQLField, GraphQLObjectType)

app = APIFlask(__name__)

# setup basic graphql schema 
query = PersonType = GraphQLObjectType('Person', {
        'name': GraphQLField(GraphQLString),
    })

schema = GraphQLSchema(query=query)

app.add_url_rule('/graphql', view_func=GraphQLView.as_view(
    'graphql',
    schema=schema,
    graphiql=True,
))
```

2. Run the flask app 
```python -m flask run```

3. Visit the AP documentation at `http://localhost:5000/docs:`


## Expected Result: 
Either the '/graphql' endpoint is ignored, or docs are generated for the endpoint. 

## Actual Results: 
`KeyError: 'get'`

Truncated Stacktrace: 
```bash
  File "...\venv\lib\site-packages\apiflask\route.py", line 39, in record_spec_for_view_class
    method = view_class.__dict__[method_name.lower()]
KeyError: 'get'
```

Environment:

- Python version: 3.9.13
- Flask version: 2.1.3
- APIFlask version: 1.1.3

## proposed solution 
From my understanding, apiflask has support for flask `MethodView`. This seems to be implemented in `route.py`.  

`GraphQLView` is a `View` not a `MethodView`. 

This error seems to be caused by apiflask treating the `GraphQLView` like it is a `MethodView`. 

If this is the issue, then maybe `add_url_rule` in route.py could check if a class is a `View` and  skip it.


Pull request:
explicitly check if view_func.view_class is MethodViewType in add_url_rule
* fixes #379 

Checklist:

- [X] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the `docs` folder and in code docstring.
- [X] Add an entry in `CHANGES.md` summarizing the change and linking to the issue and your username.
- [ ] Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
- [X] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index c39ae80..8398a83 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,12 +11,14 @@ Released: -
 - Change the status code of request validation error from 400 to 422 ([issue #345][issue_345]).
 - Improve the default bypassing rules to support bypass blueprint's static endpoint and
   Flask-DebugToolbar ([issue #344][issue_344], [issue #369][issue_369]).
+- Explicitly check if `view_func.view_class` is `MethodViewType` in `add_url_rule` ([issue #379][issue_379]).
 
 [issue_350]: https://github.com/apiflask/apiflask/issues/350
 [issue_349]: https://github.com/apiflask/apiflask/issues/349
 [issue_345]: https://github.com/apiflask/apiflask/issues/345
 [issue_344]: https://github.com/apiflask/apiflask/issues/344
 [issue_369]: https://github.com/apiflask/apiflask/issues/369
+[issue_379]: https://github.com/apiflask/apiflask/issues/379
 
 
 ## Version 1.1.3
diff --git a/src/apiflask/route.py b/src/apiflask/route.py
index c365ff8..46ef229 100644
--- a/src/apiflask/route.py
+++ b/src/apiflask/route.py
@@ -76,9 +76,15 @@ def route_patch(cls):
         is_view_class: bool = False
 
         if hasattr(view_func, 'view_class'):
+            view_class = view_func.view_class  # type: ignore
             # a function returned by MethodViewClass.as_view()
             is_view_class = True
-            view_class = view_func.view_class  # type: ignore
+            if not isinstance(view_class, type(MethodView)):
+                # avoid recording spec for view class
+                is_view_class = False
+                # avoid auto response being added to bare view
+                view_func._spec = {'hide': True}  # type: ignore
+
         elif isinstance(view_func, type(MethodView)):
             # a MethodView class passed with the route decorator
             is_view_class = True


Test patch:
diff --git a/tests/test_route.py b/tests/test_route.py
index fb2ee4f..c6cf84a 100644
--- a/tests/test_route.py
+++ b/tests/test_route.py
@@ -1,5 +1,6 @@
 import pytest
 from flask.views import MethodView
+from flask.views import View
 from openapi_spec_validator import validate_spec
 
 from .schemas import Foo
@@ -212,3 +213,19 @@ def test_view_endpoint_contains_dot(app, client):
     assert rv.status_code == 200
     validate_spec(rv.json)
     assert rv.json['paths']['/']['get']
+
+
+def test_add_url_rule_with_view_as_view(app, client):
+    """Flask Views should be ignored by add_url_rule"""
+    class Foo(View):
+        def dispatch_request(self):
+            pass
+
+    foo_view = Foo.as_view('foo')
+
+    app.add_url_rule('/foo', view_func=foo_view)
+
+    rv = client.get('/openapi.json')
+    assert rv.status_code == 200
+
+    assert '/foo' not in rv.json['paths']