From f0293e686de6d2069ded7be5f13e40c60150b211 Mon Sep 17 00:00:00 2001 From: Cellan Hall <60790416+Ce11an@users.noreply.github.com> Date: Tue, 16 May 2023 09:41:06 +0100 Subject: [PATCH] Implement `mypy` (#60) --- .github/actions/action.yml | 46 ++++++++++++++++++++ .github/workflows/ruff.yml | 16 ------- .github/workflows/stability.yml | 38 +++++++++++++++++ poetry.lock | 52 ++++++++++++++++++++++- pyproject.toml | 20 ++++++++- surrealdb/http.py | 52 ++++++++++++----------- surrealdb/ws.py | 75 ++++++++++++--------------------- 7 files changed, 207 insertions(+), 92 deletions(-) create mode 100644 .github/actions/action.yml delete mode 100644 .github/workflows/ruff.yml create mode 100644 .github/workflows/stability.yml diff --git a/.github/actions/action.yml b/.github/actions/action.yml new file mode 100644 index 00000000..a49289a7 --- /dev/null +++ b/.github/actions/action.yml @@ -0,0 +1,46 @@ +name: Build Poetry + +description: 'Build Poetry' + +runs: + using: 'composite' + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Get full Python version + id: full-python-version + run: echo "version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")" >> $GITHUB_OUTPUT + shell: bash + + - name: Bootstrap poetry + run: curl -sL https://install.python-poetry.org | python - -y + shell: bash + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + shell: bash + + - name: Configure poetry + run: poetry config virtualenvs.in-project true + shell: bash + + - name: Set up cache + uses: actions/cache@v3 + id: cache + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ inputs.working-directory }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + run: timeout 10s poetry run pip --version || rm -rf .venv + shell: bash + + - name: Install Dependencies + run: poetry install + shell: bash diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml deleted file mode 100644 index 74d4a063..00000000 --- a/.github/workflows/ruff.yml +++ /dev/null @@ -1,16 +0,0 @@ -# https://beta.ruff.rs -name: ruff -on: - push: - branches: - - main - pull_request: - branches: - - main -jobs: - ruff: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - run: pip install --user ruff - - run: ruff --format=github . \ No newline at end of file diff --git a/.github/workflows/stability.yml b/.github/workflows/stability.yml new file mode 100644 index 00000000..aced72d9 --- /dev/null +++ b/.github/workflows/stability.yml @@ -0,0 +1,38 @@ +name: Code Stability + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: stability-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + stability: + name: Code Stability + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build Python + uses: ./.github/actions + + - name: Install tools + run: poetry install --only dev + + - id: ruff + if: always() + run: poetry run ruff --format=github surrealdb/ + + - id: Black + if: always() + run: poetry run black surrealdb/ --check --verbose --diff --color + + - id: mypy + if: always() + run: poetry run mypy surrealdb/ diff --git a/poetry.lock b/poetry.lock index 34e9fe1f..3890fee8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "anyio" @@ -262,6 +262,54 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +[[package]] +name = "mypy" +version = "1.2.0" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d"}, + {file = "mypy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"}, + {file = "mypy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e"}, + {file = "mypy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a"}, + {file = "mypy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb"}, + {file = "mypy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937"}, + {file = "mypy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9"}, + {file = "mypy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602"}, + {file = "mypy-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140"}, + {file = "mypy-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336"}, + {file = "mypy-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e"}, + {file = "mypy-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950"}, + {file = "mypy-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6"}, + {file = "mypy-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5"}, + {file = "mypy-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f"}, + {file = "mypy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521"}, + {file = "mypy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238"}, + {file = "mypy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48"}, + {file = "mypy-1.2.0-py3-none-any.whl", hash = "sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394"}, + {file = "mypy-1.2.0.tar.gz", hash = "sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -707,4 +755,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "2a8e5560e9083d7412998ed61dc1f9e76b5472b2503216d0f9d93ba57a4c4036" +content-hash = "ce076ce21bddacbb95f0bf3450354434ac00110b34750027e138072e1dcad2ff" diff --git a/pyproject.toml b/pyproject.toml index e4bc5c4c..e43f80f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ websockets = "^10.4" pre-commit = ">=2.20.0" black = ">=22.8.0" ruff = ">=0.0.245" +mypy = ">=1.2.0" [build-system] requires = ["poetry-core>=1.0.0"] @@ -73,7 +74,7 @@ ignore = [ "D104", # Missing docstring in public package "D107", # Missing docstring in __init__ "D205", # 1 blank line required between summary line and description - "D212", # Multi-line docstring summary should start at the first line + "D212", # Multi-line docstring summary should start at the first line "N805", # First argument of a method should be named self "N818", # Exception name ... should be named with an Error suffix "UP035" # Typing deprecations @@ -108,3 +109,20 @@ convention = "google" [tool.ruff.pyupgrade] keep-runtime-typing = true + +[tool.mypy] +python_version = 3.8 +pretty = true +show_traceback = true +color_output = true +check_untyped_defs = true +disallow_incomplete_defs = true +ignore_missing_imports = true +implicit_reexport = true +strict_equality = true +strict_optional = false +warn_no_return = true +warn_redundant_casts = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true diff --git a/surrealdb/http.py b/surrealdb/http.py index aa12a854..5a1e83d7 100644 --- a/surrealdb/http.py +++ b/surrealdb/http.py @@ -99,14 +99,14 @@ async def __aexit__( traceback: Optional[TracebackType] = None, ) -> None: """Disconnect from the http client when exiting the context manager.""" - await self.disconnect() + await self.close() async def connect(self) -> None: - """Connects to a local or remote database endpoint.""" + """Connect to a local or remote database endpoint.""" await self._http.__aenter__() async def close(self) -> None: - """Closes the persistent connection to the database.""" + """Close the persistent connection to the database.""" await self._http.aclose() async def _request( @@ -132,10 +132,11 @@ async def _request( # Missing method - authenticate # Missing method - let # Missing method - merge - #TODO fix signup and signin methods - + # TODO fix signup and signin methods + # TODO: Review type: ignore comments. + async def signup(self, vars: Dict[str, Any]) -> str: - """Signs this connection up to a specific authentication scope. + """Sign this connection up to a specific authentication scope. Args: vars: Variables used in a signup query. @@ -143,11 +144,13 @@ async def signup(self, vars: Dict[str, Any]) -> str: Examples: await db.signup({"user": "bob", "pass": "123456"}) """ - response = await self._request(method="POST", uri="/signup", data=json.dumps(vars)) - return response + response = await self._request( + method="POST", uri="/signup", data=json.dumps(vars) + ) + return response # type: ignore async def signin(self, vars: Dict[str, Any]) -> str: - """Signs this connection in to a specific authentication scope. + """Sign this connection in to a specific authentication scope. Args: vars: Variables used in a signin query. @@ -155,13 +158,15 @@ async def signin(self, vars: Dict[str, Any]) -> str: Examples: await db.signin({"user": "root", "pass": "root"}) """ - response = await self._request(method="POST", uri="/signin", data=json.dumps(vars)) - return response + response = await self._request( + method="POST", uri="/signin", data=json.dumps(vars) + ) + return response # type: ignore async def query( self, sql: str, vars: Optional[Dict[str, Any]] = None ) -> List[Dict[str, Any]]: - """Runs a set of SurrealQL statements against the database. + """Run a set of SurrealQL statements against the database. Args: sql: Specifies the SurrealQL statements. @@ -181,10 +186,10 @@ async def query( result[1]['result'] """ response = await self._request(method="POST", uri="/sql", data=sql, params=vars) - return response + return response # type: ignore async def select(self, thing: str) -> List[Dict[str, Any]]: - """Selects all records in a table (or other entity), + """Select all records in a table (or other entity), or a specific record, in the database. This function will run the following query in the database: @@ -210,7 +215,7 @@ async def select(self, thing: str) -> List[Dict[str, Any]]: ) if not response and record_id is not None: raise SurrealException(f"Key {record_id} not found in table {table}") - return response[0]['result'] + return response[0]["result"] # type: ignore async def create(self, thing: str, data: Optional[Dict[str, Any]] = None) -> str: """Create a record in the database. @@ -243,11 +248,10 @@ async def create(self, thing: str, data: Optional[Dict[str, Any]] = None) -> str ) if not response and record_id is not None: raise SurrealException(f"Key {record_id} not found in table {table}") - return response[0]['result'] - + return response[0]["result"] # type: ignore async def update(self, thing: str, data: Any) -> Dict[str, Any]: - """Updates all records in a table, or a specific record, in the database. + """Update all records in a table, or a specific record, in the database. This function replaces the current document / record data with the specified data. @@ -278,10 +282,10 @@ async def update(self, thing: str, data: Any) -> Dict[str, Any]: uri=f"/key/{table}/{record_id}" if record_id else f"/key/{table}", data=json.dumps(data, ensure_ascii=False), ) - return response[0]['result'] + return response[0]["result"] # type: ignore async def patch(self, thing: str, data: Any) -> Dict[str, Any]: - """Applies JSON Patch changes to all records, or a specific record, in the database. + """Apply JSON Patch changes to all records, or a specific record, in the database. This function patches the current document / record data with the specified JSON Patch data. @@ -311,10 +315,10 @@ async def patch(self, thing: str, data: Any) -> Dict[str, Any]: uri=f"/key/{table}/{record_id}" if record_id else f"/key/{table}", data=json.dumps(data, ensure_ascii=False), ) - return response[0]['result'] + return response[0]["result"] # type: ignore - async def delete(self, thing: str) -> None: - """Deletes all records in a table, or a specific record, from the database. + async def delete(self, thing: str) -> List[Dict[str, Any]]: + """Delete all records in a table, or a specific record, from the database. This function will run the following query in the database: delete * from $thing @@ -333,4 +337,4 @@ async def delete(self, thing: str) -> None: method="DELETE", uri=f"/key/{table}/{record_id}" if record_id else f"/key/{table}", ) - return response + return response # type: ignore diff --git a/surrealdb/ws.py b/surrealdb/ws.py index 13f6a73d..ee3a587f 100644 --- a/surrealdb/ws.py +++ b/surrealdb/ws.py @@ -189,10 +189,10 @@ class Surreal: """ - def __init__(self, url: Optional[str] = None, token: Optional[str] = None) -> None: + def __init__(self, url: str) -> None: self.url = url - self.token = token self.client_state = ConnectionState.CONNECTING + self.token: Optional[str] = None self.ws: Optional[websockets.WebSocketClientProtocol] = None # type: ignore async def __aenter__(self) -> Surreal: @@ -219,43 +219,21 @@ async def __aexit__( """ await self.close() - async def connect(self, url: Optional[str] = None) -> None: - """Connects to a local or remote database endpoint. - - Args: - url: The URL of the Surreal server. + async def connect(self) -> None: + """Connect to a local or remote database endpoint. Examples: Connect to a local endpoint db = Surreal() await db.connect('ws://127.0.0.1:8000/rpc') await db.signin({"user": "root", "pass": "root"}) - - Connect to a remote endpoint - db = Surreal() - await db.connect('http://cloud.surrealdb.com/rpc') - await db.signin({"user": "root", "pass": "root"}) """ - if url is not None: - self.url = url - else: - self.url - # helping people when they type the url in wrong - if "http" in self.url: - self.url = self.url.replace("http://", "ws://") - elif "https" in self.url: - self.url = self.url.replace("https://", "wss://") - if "/rpc" not in self.url: - self.url = "".join([self.url, "/rpc"]) self.ws = await websockets.connect(self.url) # type: ignore self.client_state = ConnectionState.CONNECTED - # Missing method - wait - Waits for the connection to the database to succeed - # Not sure if needed though - async def close(self) -> None: - """Closes the persistent connection to the database.""" - await self.ws.close() # type: ignore + """Close the persistent connection to the database.""" + await self.ws.close() self.client_state = ConnectionState.DISCONNECTED async def use(self, namespace: str, database: str) -> None: @@ -274,7 +252,7 @@ async def use(self, namespace: str, database: str) -> None: _validate_response(response) async def signup(self, vars: Dict[str, Any]) -> str: - """Signs this connection up to a specific authentication scope. + """Sign this connection up to a specific authentication scope. Args: vars: Variables used in a signup query. @@ -293,7 +271,7 @@ async def signup(self, vars: Dict[str, Any]) -> str: return self.token async def signin(self, vars: Dict[str, Any]) -> str: - """Signs this connection in to a specific authentication scope. + """Sign this connection in to a specific authentication scope. Args: vars: Variables used in a signin query. @@ -312,7 +290,7 @@ async def signin(self, vars: Dict[str, Any]) -> str: return self.token async def invalidate(self) -> None: - """Invalidates the authentication for the current connection.""" + """Invalidate the authentication for the current connection.""" response = await self._send_receive( Request( id=generate_uuid(), @@ -323,7 +301,7 @@ async def invalidate(self) -> None: self.token = None async def authenticate(self, token: str) -> None: - """Authenticates the current connection with a JWT token. + """Authenticate the current connection with a JWT token. Args: token: The token to use for the connection. @@ -337,7 +315,7 @@ async def authenticate(self, token: str) -> None: _validate_response(response, SurrealAuthenticationException) async def let(self, key: str, value: Any) -> None: - """Assigns a value as a parameter for this connection. + """Assign a value as a parameter for this connection. Args: key: Specifies the name of the variable. @@ -392,7 +370,7 @@ async def set(self, key: str, value: Any) -> None: async def query( self, sql: str, vars: Optional[Dict[str, Any]] = None ) -> List[Dict[str, Any]]: - """Runs a set of SurrealQL statements against the database. + """Run a set of SurrealQL statements against the database. Args: sql: Specifies the SurrealQL statements. @@ -422,7 +400,7 @@ async def query( return success.result async def select(self, thing: str) -> List[Dict[str, Any]]: - """Selects all records in a table (or other entity), + """Select all records in a table (or other entity), or a specific record, in the database. This function will run the following query in the database: @@ -485,9 +463,9 @@ async def create( return success.result async def update( - self, thing, data: Optional[Dict[str, Any]] + self, thing: str, data: Optional[Dict[str, Any]] ) -> List[Dict[str, Any]]: - """Updates all records in a table, or a specific record, in the database. + """Update all records in a table, or a specific record, in the database. This function replaces the current document / record data with the specified data. @@ -527,7 +505,7 @@ async def update( async def merge( self, thing: str, data: Optional[Dict[str, Any]] ) -> List[Dict[str, Any]]: - """Modifies by deep merging all records in a table, or a specific record, in the database. + """Modify by deep merging all records in a table, or a specific record, in the database. This function merges the current document / record data with the specified data. @@ -561,16 +539,15 @@ async def merge( params=(thing,) if data is None else (thing, data), ), ) - _validate_response(response, SurrealPermissionException) - # success: ResponseSuccess = _validate_response( - # response, SurrealPermissionException - # ) - # return success.result + success: ResponseSuccess = _validate_response( + response, SurrealPermissionException + ) + return success.result async def patch( self, thing: str, data: Optional[Dict[str, Any]] ) -> List[Dict[str, Any]]: - """Applies JSON Patch changes to all records, or a specific record, in the database. + """Apply JSON Patch changes to all records, or a specific record, in the database. This function patches the current document / record data with the specified JSON Patch data. @@ -607,7 +584,7 @@ async def patch( return success.result async def delete(self, thing: str) -> List[Dict[str, Any]]: - """Deletes all records in a table, or a specific record, from the database. + """Delete all records in a table, or a specific record, from the database. This function will run the following query in the database: delete * from $thing @@ -633,7 +610,7 @@ async def delete(self, thing: str) -> List[Dict[str, Any]]: # Surreal library methods - undocumented but implemented in js library async def info(self) -> Optional[Dict[str, Any]]: - """Retreive info about the current Surreal instance. + """Retrieve info about the current Surreal instance. Returns: The information of the Surreal server. @@ -715,10 +692,10 @@ async def _send(self, request: Request) -> None: Exception: If the client is not connected to the Surreal server. """ self._validate_connection() - await self.ws.send(json.dumps(request.dict(), ensure_ascii=False)) # type: ignore + await self.ws.send(json.dumps(request.dict(), ensure_ascii=False)) async def _recv(self) -> Union[ResponseSuccess, ResponseError]: - """Receives a response from the Surreal server. + """Receive a response from the Surreal server. Returns: The response from the Surreal server. @@ -728,7 +705,7 @@ async def _recv(self) -> Union[ResponseSuccess, ResponseError]: Exception: If the response contains an error. """ self._validate_connection() - response = json.loads(await self.ws.recv()) # type: ignore + response = json.loads(await self.ws.recv()) if response.get("error"): return ResponseError(**response["error"]) return ResponseSuccess(**response)