Skip to content

Commit

Permalink
fix: add support for multi-section version strings
Browse files Browse the repository at this point in the history
This fixes an issue where an exception would occur when a python dependency had a version that did not conform to semver specs, such as 1.18.3.9
  • Loading branch information
SteveShani committed Jun 25, 2024
1 parent 6decc45 commit d71bb3f
Show file tree
Hide file tree
Showing 7 changed files with 566 additions and 2 deletions.
3 changes: 2 additions & 1 deletion lib/analyzer/applications/python/pip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { eventLoopSpinner } from "event-loop-spinner";
import * as path from "path";
import * as semver from "semver";
import { DepGraphFact } from "../../../facts";
import { compareVersions } from "../../../python-parser/common";
import { getPackageInfo } from "../../../python-parser/metadata-parser";
import { getRequirements } from "../../../python-parser/requirements-parser";
import {
Expand Down Expand Up @@ -115,7 +116,7 @@ export async function pipFilesToScannedProjects(
// pre-sort each package name by version, descending
for (const name of Object.keys(metadataItems)) {
metadataItems[name].sort((v1, v2) => {
return semver.rcompare(v1.version, v2.version);
return compareVersions(v1.version, v2.version);
});
}

Expand Down
39 changes: 39 additions & 0 deletions lib/python-parser/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,42 @@ export function specifierValidRange(
const versionPartsLength = version.split(".").length;
return versionPartsLength === 2 ? "^" : "~";
}

// This regex is used to extract the "semver" part from the version string.
// See the tests for a better understanding (Also the reason why this is exported)
export const VERSION_EXTRACTION_REGEX = /(?<VERSION>(\d+\.)*\d+).*/;

function compareArrays(v1: number[], v2: number[]) {
const max = v1.length > v2.length ? v1.length : v2.length;
for (let i = 0; i < max; i++) {
if ((v1[i] || 0) < (v2[i] || 0)) {
return 1;
}
if ((v1[i] || 0) > (v2[i] || 0)) {
return -1;
}
}
return 0;
}

/**
* This function was taken from a different semver library and slightly modified.
* If passed to Array.prototype.sort, versions will be sorted in descending order.
*/
export function compareVersions(version1: string, version2: string) {
const v1Match = VERSION_EXTRACTION_REGEX.exec(version1);
const v2Match = VERSION_EXTRACTION_REGEX.exec(version2);

if (v1Match === null || v2Match === null) {
return 0;
}

const v1 = v1Match
.groups!.VERSION.split(".")
.map((part) => Number.parseInt(part, 10));
const v2 = v2Match
.groups!.VERSION.split(".")
.map((part) => Number.parseInt(part, 10));

return compareArrays(v1, v2);
}
1 change: 1 addition & 0 deletions test/fixtures/python/non-semver-versions/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
six~=1.14
flask>=2.2.0
rpc.py==0.4.2
other.py>=7.4.2.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
Metadata-Version: 2.1
Name: other.py
Version: 7.4.2.15
Summary: An fast and powerful RPC framework based on ASGI/WSGI.
Home-page: https://github.com/abersheeran/rpc.py
License: Apache-2.0
Author: abersheeran
Author-email: [email protected]
Requires-Python: >=3.7,<4.0
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
Classifier: Typing :: Typed
Provides-Extra: client
Provides-Extra: full
Provides-Extra: msgpack
Provides-Extra: type
Requires-Dist: httpx (>=0.16.0,<0.17.0); extra == "client" or extra == "full"
Requires-Dist: msgpack (>=1.0.0,<2.0.0); extra == "full" or extra == "msgpack"
Requires-Dist: pydantic (>=1.7,<2.0); extra == "full" or extra == "type"
Requires-Dist: typing-extensions (>=3.7.4,<4.0.0); python_version < "3.8"
Project-URL: Repository, https://github.com/abersheeran/rpc.py
Description-Content-Type: text/markdown

# rpc.py

[![Codecov](https://img.shields.io/codecov/c/github/abersheeran/rpc.py?style=flat-square)](https://codecov.io/gh/abersheeran/rpc.py)

An fast and powerful RPC framework based on ASGI/WSGI.

Based on WSGI/ASGI, you can deploy the rpc.py server to any server and use http2 to get better performance.

## Install

Install from PyPi:

```bash
pip install rpc.py

# need use client
pip install rpc.py[client]

# need use pydantic type hint or OpenAPI docs
pip install rpc.py[type]

# need use msgpack to serializer
pip install rpc.py[msgpack]

# or install all dependencies
pip install rpc.py[full]
```

Install from github:

```bash
pip install git+https://github.com/abersheeran/[email protected]
```

## Usage

### Server side:

<details markdown="1">
<summary>Use <code>ASGI</code> mode to register <code>async def</code>...</summary>

```python
from typing import AsyncGenerator

import uvicorn
from rpcpy import RPC

app = RPC(mode="ASGI")


@app.register
async def none() -> None:
return


@app.register
async def sayhi(name: str) -> str:
return f"hi {name}"


@app.register
async def yield_data(max_num: int) -> AsyncGenerator[int, None]:
for i in range(max_num):
yield i


if __name__ == "__main__":
uvicorn.run(app, interface="asgi3", port=65432)
```
</details>

OR

<details markdown="1">
<summary>Use <code>WSGI</code> mode to register <code>def</code>...</summary>

```python
from typing import Generator

import uvicorn
from rpcpy import RPC

app = RPC()


@app.register
def none() -> None:
return


@app.register
def sayhi(name: str) -> str:
return f"hi {name}"


@app.register
def yield_data(max_num: int) -> Generator[int, None, None]:
for i in range(max_num):
yield i


if __name__ == "__main__":
uvicorn.run(app, interface="wsgi", port=65432)
```
</details>

### Client side:

Notice: Regardless of whether the server uses the WSGI mode or the ASGI mode, the client can freely use the asynchronous or synchronous mode.

<details markdown="1">
<summary>Use <code>httpx.Client()</code> mode to register <code>def</code>...</summary>

```python
from typing import Generator

import httpx
from rpcpy.client import Client

app = Client(httpx.Client(), base_url="http://127.0.0.1:65432/")


@app.remote_call
def none() -> None:
...


@app.remote_call
def sayhi(name: str) -> str:
...


@app.remote_call
def yield_data(max_num: int) -> Generator[int, None, None]:
yield
```
</details>

OR

<details markdown="1">
<summary>Use <code>httpx.AsyncClient()</code> mode to register <code>async def</code>...</summary>

```python
from typing import AsyncGenerator

import httpx
from rpcpy.client import Client

app = Client(httpx.AsyncClient(), base_url="http://127.0.0.1:65432/")


@app.remote_call
async def none() -> None:
...


@app.remote_call
async def sayhi(name: str) -> str:
...


@app.remote_call
async def yield_data(max_num: int) -> AsyncGenerator[int, None]:
yield
```
</details>

### Sub-route

If you need to deploy the rpc.py server under `example.com/sub-route/*`, you need to set `RPC(prefix="/sub-route/")` and modify the `Client(base_path=https://example.com/sub-route/)`.

### Serialization

Currently supports three serializers, JSON, Pickle and Msgpack. JSON is used by default. You can override the default `JSONSerializer` with parameters.

```python
import httpx
from rpcpy import RPC
from rpcpy.client import Client
from rpcpy.serializers import PickleSerializer, MsgpackSerializer

RPC(response_serializer=MsgpackSerializer())
# Or
Client(
...,
request_serializer=PickleSerializer(),
)
```

## Type hint and OpenAPI Doc

Thanks to the great work of [pydantic](https://pydantic-docs.helpmanual.io/), which makes rpc.py allow you to use type annotation to annotate the types of function parameters and response values, and perform type verification and JSON serialization . At the same time, it is allowed to generate openapi documents for human reading.

### OpenAPI Documents

If you want to open the OpenAPI document, you need to initialize `RPC` like this `RPC(openapi={"title": "TITLE", "description": "DESCRIPTION", "version": "v1"})`.

Then, visit the `"{prefix}openapi-docs"` of RPC and you will be able to see the automatically generated OpenAPI documentation. (If you do not set the `prefix`, the `prefix` is `"/"`)

## Limitations

Currently, file upload is not supported, but you can do this by passing a `bytes` object.

Loading

0 comments on commit d71bb3f

Please sign in to comment.