Skip to content

Commit

Permalink
feat: prevent Writer Framework version mismatch local/cloud
Browse files Browse the repository at this point in the history
* fix: comment from review
  • Loading branch information
FabienArcellier committed Nov 11, 2024
1 parent 3bd8645 commit aa2de38
Show file tree
Hide file tree
Showing 8 changed files with 32 additions and 1,321 deletions.
1,209 changes: 0 additions & 1,209 deletions apps/default/poetry.lock

This file was deleted.

8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ websockets = ">= 12, < 13"
writer-sdk = ">= 1.2.0, < 2"
python-multipart = ">=0.0.7, < 1"
toml = "^0.10.2"
packaging = "^24.2"


[tool.poetry.group.build]
Expand Down
2 changes: 1 addition & 1 deletion src/writer/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def deploy(path, api_key, env, verbose, force):
raise click.ClickException("The application does not have a poetry.lock file. Please run `poetry install` to generate it.")

writer_version = wf_project.writer_version(abs_path)
if wf_project.compare_semantic_version(poetry_locked_version, writer_version) == -1:
if poetry_locked_version < writer_version:
raise click.ClickException(f"locked version in poetry.lock does not match the project version use in .wf, pin a new version `poetry add writer@^{writer_version}")

env = _validate_env_vars(env)
Expand Down
10 changes: 6 additions & 4 deletions src/writer/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fastapi.responses import FileResponse
from fastapi.routing import Mount
from fastapi.staticfiles import StaticFiles
from packaging.version import Version
from pydantic import ValidationError
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState

Expand Down Expand Up @@ -645,16 +646,17 @@ def _warn_about_version_consistency(mode: ServeMode, app_path: str) -> None:
if dev_version is None:
return

run_version = wf_project.parse_semantic_version(VERSION)
if wf_project.compare_semantic_version(run_version, dev_version) == -1:
run_version = Version(VERSION)
if run_version < dev_version:
if mode == "run":
logger.warning(f"The version {run_version}, used to run the app, is older than the version {dev_version} used during development.")

if wf_project.use_pyproject(app_path):
logger.warning("You should update the version of writer in the pyproject.toml.")

if wf_project.use_requirement(app_path):
elif wf_project.use_requirement(app_path):
logger.warning("You should update the version of writer in the requirements.txt.")
else:
logger.warning("You should update the version of writer.")

if mode == "edit":
logger.warning(f"The version {VERSION}, used to edit the app, is older than the version {dev_version} used in previous development.")
Expand Down
18 changes: 0 additions & 18 deletions src/writer/ss_types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dataclasses
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union

from pydantic import BaseModel
Expand Down Expand Up @@ -210,22 +209,5 @@ class ComponentDefinition(TypedDict):
x: Optional[int]
y: Optional[int]


class WorkflowExecutionLog(BaseModel):
summary: List[Dict]

@dataclasses.dataclass
class SemanticVersion:
major: int
minor: int
fix: int
rc: Optional[int]

def __str__(self):
version = f"{self.major}.{self.minor}.{self.fix}"
if self.rc is not None:
version += f"rc{self.rc}"
return version

def version_tuple(self):
return (self.major, self.minor, self.fix, self.rc)
81 changes: 6 additions & 75 deletions src/writer/wf_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
from typing import Any, Dict, List, Tuple

import toml
from packaging.version import Version

from writer import core_ui
from writer.ss_types import ComponentDefinition, MetadataDefinition, SemanticVersion
from writer.ss_types import ComponentDefinition, MetadataDefinition

ROOTS = ['root', 'workflows_root']
COMPONENT_ROOTS = ['page', 'workflows_workflow']
Expand Down Expand Up @@ -139,77 +140,7 @@ def create_default_workflows_root(app_path: str) -> None:
logger.warning('project format has changed and has been migrated with success. components-workflows_root.jsonl has been added.')


def parse_semantic_version(version: str) -> SemanticVersion:
"""
Parses a version string into a semantic version tuple (major, minor, fix, rc)
>>> parse_semantic_version('1.0.0')
>>> parse_semantic_version('1.0.0rc1')
"""
major, minor, fix_part = version.split('.')
if 'rc' in fix_part:
fix, rc = fix_part.split('rc')
else:
fix, rc = fix_part, None

semantic_version: SemanticVersion
if rc is not None:
semantic_version = SemanticVersion(
major=int(major),
minor=int(minor),
fix=int(fix),
rc=int(rc)
)
else:
semantic_version = SemanticVersion(
major=int(major),
minor=int(minor),
fix=int(fix),
rc=None
)

return semantic_version


def compare_semantic_version(version1: SemanticVersion, version2: SemanticVersion) -> int:
"""
Compares two versions that support semantic version (major, minor, fix, rc).
* 1 when version1 > version2
* 0 when version1 == version2
* -1 if version1 < version2
>>> compare_semantic_version(SemanticVersion(1, 0, 0, None), SemanticVersion(1, 0, 0, 1)) # 1
>>> compare_semantic_version(SemanticVersion(1, 0, 0, 1), SemanticVersion(1, 0, 0, 4)) # -1
>>> compare_semantic_version(SemanticVersion(1, 0, 0, 1), SemanticVersion(1, 0, 0, 1)) # 0
"""
if version1 == version2:
return 0

v1 = version1.version_tuple()
v2 = version2.version_tuple()
if v1[:3] > v2[:3]:
return 1

if v1[:3] < v2[:3]:
return -1

if v1[3] is None and v2[3] is not None:
return 1

if v2[3] is None and v1[3] is not None:
return -1

if v1[3] > v2[3]:
return 1

if v1[3] < v2[3]:
return -1

return 0


def writer_version(app_path: str) -> typing.Optional[SemanticVersion]:
def writer_version(app_path: str) -> typing.Optional[Version]:
"""
Retrieves the writer version used during development
Expand All @@ -218,12 +149,12 @@ def writer_version(app_path: str) -> typing.Optional[SemanticVersion]:
meta, _ = read_files(app_path)
writer_version = meta.get('writer_version', None)
if writer_version:
return parse_semantic_version(writer_version)
return Version(writer_version)

return None


def poetry_locked_version(app_path: str) -> typing.Optional[SemanticVersion]:
def poetry_locked_version(app_path: str) -> typing.Optional[Version]:
"""
Retrieves the version fixed in the poetry.lock file for Writer
Expand All @@ -242,7 +173,7 @@ def poetry_locked_version(app_path: str) -> typing.Optional[SemanticVersion]:
for package in poetry_lock['package']:
if package['name'] == 'writer':
locked_version = package['version']
return parse_semantic_version(locked_version)
return Version(locked_version)

return None

Expand Down
24 changes: 14 additions & 10 deletions tests/backend/test_wf_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import tempfile
from typing import List

from packaging.version import Version
from writer import VERSION, core_ui, wf_project
from writer.ss_types import ComponentDefinition, SemanticVersion
from writer.ss_types import ComponentDefinition

from tests.backend import test_app_dir, testobsoleteapp
from tests.backend.fixtures import file_fixtures, load_fixture_content
Expand Down Expand Up @@ -128,19 +129,22 @@ def test_wf_project_migrate_obsolete_ui_json_should_migrate_ui_json_into_wf_dire
assert os.path.isfile(os.path.join(tmp_app_dir, '.wf', 'components-root.jsonl'))


def test_compare_semantic_version_should_compare_verson_with_rc():
def test_packaging_Version_should_compare_verson():
"""
Integration test that validate the behavior of packaging.version.Version
"""
# When
# Version1 > Version 2
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 0, None), SemanticVersion(0, 0, 0, 1)) == 1
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 0, 5), SemanticVersion(0, 0, 0, 3)) == 1
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 2, None), SemanticVersion(0, 0, 0, None)) == 1
assert Version("0.0.0") > Version("0.0.0rc1")
assert Version("0.0.0rc5") > Version("0.0.0rc3")
assert Version("0.0.2") > Version("0.0.0")

# Version1 == Version 2
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 0, None), SemanticVersion(0, 0, 0, None)) == 0
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 0, 4), SemanticVersion(0, 0, 0, 4)) == 0
assert Version("0.0.0") == Version("0.0.0")
assert Version("0.0.0rc4") == Version("0.0.0rc4")

# Version1 < Version 2
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 0, None), SemanticVersion(1, 0, 0, None)) == -1
assert wf_project.compare_semantic_version(SemanticVersion(0, 0, 0, 8), SemanticVersion(1, 0, 0, None)) == -1
assert wf_project.compare_semantic_version(SemanticVersion(1, 0, 0, 1), SemanticVersion(1, 0, 0, 2)) == -1
assert Version("0.0.0") < Version("1.0.0")
assert Version("0.0.0rc8") < Version("1.0.0")
assert Version("1.0.0rc1") < Version("1.0.0rc2")

0 comments on commit aa2de38

Please sign in to comment.