Skip to content

Commit

Permalink
Redesigning protostar.toml 6 — Version checker (#858)
Browse files Browse the repository at this point in the history
Currently, `protostar_version` is not properly handled by Protostar. It
shouldn't be used to detect the version of the configuration file (v1,
v2), but help a development team that uses Protostar to use compatible
versions.

### Use case
Let's say we introduce a new cheatcode in Protostar v0.5. Alice upgraded
Protostar and uses Protostar v0.5. Bob uses Protostar v0.4. Alice uses
the new cheatcode and integrates her code with the mainline. Bob pulls
the mainline and runs the code that uses the new cheatcode.

#### Note:
We should bump the minor (not micro) if forward compatibility is no
preserved (e.g. new cheatcode).

### Current behavior
Protostar crashes for Bob. 

### Expected behavior
1. Protostar tells Alice to update the declared version in the
configuration file.
2. Protostar tells Bob to upgrade Protostar.
  • Loading branch information
kasperski95 authored Sep 22, 2022
1 parent bf20a7b commit d835da0
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 31 deletions.
8 changes: 5 additions & 3 deletions protostar/configuration_file/configuration_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Any, Generic, Optional, TypeVar, Union

from protostar.protostar_exception import ProtostarException
from protostar.utils.protostar_directory import VersionType
from protostar.self import DeclaredProtostarVersionProviderProtocol, ProtostarVersion

PrimitiveTypesSupportedByConfigurationFile = Union[str, int, bool]

Expand Down Expand Up @@ -43,9 +43,11 @@ def create_file_content(
...


class ConfigurationFile(Generic[ConfigurationFileModelT]):
class ConfigurationFile(
DeclaredProtostarVersionProviderProtocol, Generic[ConfigurationFileModelT]
):
@abstractmethod
def get_min_protostar_version(self) -> Optional[VersionType]:
def get_declared_protostar_version(self) -> Optional[ProtostarVersion]:
...

@abstractmethod
Expand Down
12 changes: 6 additions & 6 deletions protostar/configuration_file/configuration_file_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pathlib import Path
from typing import Optional, Union

from protostar.utils.protostar_directory import VersionManager, VersionType
from protostar.self import ProtostarVersion, parse_protostar_version

from .configuration_file import (
CommandConfig,
Expand Down Expand Up @@ -47,15 +47,15 @@ def __init__(
"migrate",
]

def get_min_protostar_version(self) -> Optional[VersionType]:
def get_declared_protostar_version(self) -> Optional[ProtostarVersion]:
version_str = self._configuration_file_interpreter.get_attribute(
attribute_name="protostar_version",
section_name="config",
section_namespace="protostar",
)
if not version_str:
return None
return VersionManager.parse(version_str)
return parse_protostar_version(version_str)

def get_contract_names(self) -> list[str]:
contract_section = self._configuration_file_interpreter.get_section(
Expand Down Expand Up @@ -109,7 +109,7 @@ def read(
self,
) -> ConfigurationFileV1Model:
return ConfigurationFileV1Model(
protostar_version=self._get_min_protostar_version_str(),
protostar_version=self._get_declared_protostar_version_str(),
libs_path_str=self._get_libs_path_str(),
contract_name_to_path_strs=self._get_contract_name_to_path_strs(),
command_name_to_config=self._get_command_name_to_config(),
Expand All @@ -118,8 +118,8 @@ def read(
profile_name_to_shared_command_config=self._get_profile_name_to_shared_command_config(),
)

def _get_min_protostar_version_str(self) -> Optional[str]:
version = self.get_min_protostar_version()
def _get_declared_protostar_version_str(self) -> Optional[str]:
version = self.get_declared_protostar_version()
if not version:
return None
return str(version)
Expand Down
8 changes: 4 additions & 4 deletions protostar/configuration_file/configuration_file_v1_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from protostar.configuration_file.configuration_legacy_toml_interpreter import (
ConfigurationLegacyTOMLInterpreter,
)
from protostar.utils import VersionManager
from protostar.self import parse_protostar_version

from .configuration_file_v1 import (
ConfigurationFile,
Expand Down Expand Up @@ -56,10 +56,10 @@ def configuration_file_fixture(
""",
],
)
def test_retrieving_min_protostar_version(configuration_file: ConfigurationFile):
result = configuration_file.get_min_protostar_version()
def test_retrieving_declared_protostar_version(configuration_file: ConfigurationFile):
result = configuration_file.get_declared_protostar_version()

assert result == VersionManager.parse("0.1.2")
assert result == parse_protostar_version("0.1.2")


@pytest.mark.parametrize(
Expand Down
16 changes: 7 additions & 9 deletions protostar/configuration_file/configuration_file_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing_extensions import Self

from protostar.utils.protostar_directory import VersionManager, VersionType
from protostar.self import ProtostarVersion, parse_protostar_version

from .configuration_file import (
CommandConfig,
Expand All @@ -23,7 +23,7 @@

@dataclass
class ConfigurationFileV2Model:
min_protostar_version: Optional[str]
protostar_version: Optional[str]
contract_name_to_path_strs: dict[ContractName, list[str]]
project_config: CommandConfig
command_name_to_config: CommandNameToConfig
Expand All @@ -32,15 +32,15 @@ class ConfigurationFileV2Model:

@classmethod
# pylint: disable=invalid-name
def from_v1(cls, v1: ConfigurationFileV1Model, min_protostar_version: str) -> Self:
def from_v1(cls, v1: ConfigurationFileV1Model, protostar_version: str) -> Self:
project_config = v1.shared_command_config
if v1.libs_path_str:
project_config = {
**{"lib-path": v1.libs_path_str},
**v1.shared_command_config,
}
return cls(
min_protostar_version=min_protostar_version,
protostar_version=protostar_version,
contract_name_to_path_strs=v1.contract_name_to_path_strs,
command_name_to_config=v1.command_name_to_config,
profile_name_to_commands_config=v1.profile_name_to_commands_config,
Expand All @@ -64,13 +64,13 @@ def __init__(
self._configuration_file_reader = configuration_file_reader
self._filename = filename

def get_min_protostar_version(self) -> Optional[VersionType]:
def get_declared_protostar_version(self) -> Optional[ProtostarVersion]:
version_str = self._configuration_file_reader.get_attribute(
attribute_name="min-protostar-version", section_name="project"
)
if not version_str:
return None
return VersionManager.parse(version_str)
return parse_protostar_version(version_str)

def get_contract_names(self) -> list[str]:
contract_section = self._configuration_file_reader.get_section("contracts")
Expand Down Expand Up @@ -145,9 +145,7 @@ def create_file_content(
@staticmethod
def _prepare_project_section_data(model: ConfigurationFileV2Model) -> dict:
project_config_section = {}
project_config_section["min-protostar-version"] = str(
model.min_protostar_version
)
project_config_section["min-protostar-version"] = str(model.protostar_version)
project_config_section: dict = {
**project_config_section,
**model.project_config,
Expand Down
18 changes: 9 additions & 9 deletions protostar/configuration_file/configuration_file_v2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from protostar.configuration_file.configuration_toml_interpreter import (
ConfigurationTOMLInterpreter,
)
from protostar.utils.protostar_directory import VersionManager
from protostar.self import parse_protostar_version

from .configuration_file import (
ConfigurationFile,
Expand Down Expand Up @@ -67,10 +67,10 @@ def configuration_file_fixture(project_root_path: Path, protostar_toml_content:
)


def test_retrieving_min_protostar_version(configuration_file: ConfigurationFile):
min_protostar_version = configuration_file.get_min_protostar_version()
def test_retrieving_declared_protostar_version(configuration_file: ConfigurationFile):
declared_protostar_version = configuration_file.get_declared_protostar_version()

assert min_protostar_version == VersionManager.parse("9.9.9")
assert declared_protostar_version == parse_protostar_version("9.9.9")


def test_retrieving_contract_names(configuration_file: ConfigurationFile):
Expand Down Expand Up @@ -122,7 +122,7 @@ def test_saving_configuration(
):
content_configurator = configuration_file
configuration_file_v2_model = ConfigurationFileV2Model(
min_protostar_version="9.9.9",
protostar_version="9.9.9",
project_config={
"lib-path": "lib",
"no-color": True,
Expand Down Expand Up @@ -163,10 +163,10 @@ def test_transforming_model_v1_into_v2():
profile_name_to_shared_command_config={"devnet": {"arg_name": 24}},
)

model_v2 = ConfigurationFileV2Model.from_v1(model_v1, min_protostar_version="0.4.0")
model_v2 = ConfigurationFileV2Model.from_v1(model_v1, protostar_version="0.4.0")

assert model_v2 == ConfigurationFileV2Model(
min_protostar_version="0.4.0",
protostar_version="0.4.0",
command_name_to_config={"deploy": {"arg_name": 21}},
contract_name_to_path_strs={"main": ["src/main.cairo"]},
project_config={"arg_name": 42, "lib-path": "lib"},
Expand Down Expand Up @@ -226,7 +226,7 @@ def test_transforming_file_v1_into_v2(protostar_toml_content: str):
filename="_",
).create_file_content(
content_builder=ConfigurationTOMLContentBuilder(),
model=ConfigurationFileV2Model.from_v1(model_v1, min_protostar_version="9.9.9"),
model=ConfigurationFileV2Model.from_v1(model_v1, protostar_version="9.9.9"),
)

assert transformed_protostar_toml == protostar_toml_content
Expand All @@ -237,7 +237,7 @@ def test_saving_in_particular_order(
):
content_configurator = configuration_file
configuration_file_v2_model = ConfigurationFileV2Model(
min_protostar_version="9.9.9",
protostar_version="9.9.9",
project_config={
"lib-path": "./lib",
"cairo-path": ["bar"],
Expand Down
5 changes: 5 additions & 0 deletions protostar/self/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .protostar_compatibility_with_project_checker import (
DeclaredProtostarVersionProviderProtocol,
ProtostarVersion,
parse_protostar_version,
)
56 changes: 56 additions & 0 deletions protostar/self/protostar_compatibility_with_project_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from enum import Enum, auto
from typing import Optional, Protocol

from packaging import version

ProtostarVersion = version.Version


class DeclaredProtostarVersionProviderProtocol(Protocol):
def get_declared_protostar_version(self) -> Optional[ProtostarVersion]:
...


class ProtostarVersionProviderProtocol(Protocol):
def get_protostar_version(self) -> ProtostarVersion:
...


class CompatibilityCheckResult(Enum):
COMPATIBLE = auto()
OUTDATED_PROTOSTAR = auto()
OUTDATED_DECLARED_VERSION = auto()
FAILURE = auto()


class ProtostarCompatibilityWithProjectChecker:
def __init__(
self,
protostar_version_provider: ProtostarVersionProviderProtocol,
declared_protostar_version_provider: DeclaredProtostarVersionProviderProtocol,
) -> None:
self._protostar_version_provider = protostar_version_provider
self._declared_protostar_version_provider = declared_protostar_version_provider

def check_compatibility(self) -> CompatibilityCheckResult:
protostar_version = self._protostar_version_provider.get_protostar_version()
declared_protostar_version = (
self._declared_protostar_version_provider.get_declared_protostar_version()
)
if declared_protostar_version is None:
return CompatibilityCheckResult.FAILURE
if (
declared_protostar_version.major == protostar_version.major
and declared_protostar_version.minor == protostar_version.minor
and declared_protostar_version.micro <= protostar_version.micro
):
return CompatibilityCheckResult.COMPATIBLE
if declared_protostar_version < protostar_version:
return CompatibilityCheckResult.OUTDATED_DECLARED_VERSION
return CompatibilityCheckResult.OUTDATED_PROTOSTAR


def parse_protostar_version(value: str) -> ProtostarVersion:
result = version.parse(value)
assert isinstance(result, ProtostarVersion)
return result
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest

from .protostar_compatibility_with_project_checker import (
CompatibilityCheckResult,
DeclaredProtostarVersionProviderProtocol,
ProtostarCompatibilityWithProjectChecker,
ProtostarVersionProviderProtocol,
parse_protostar_version,
)


class DeclaredProtostarVersionProviderDouble(DeclaredProtostarVersionProviderProtocol):
def __init__(self, declared_protostar_version_str: str):
self._declared_protostar_version_str = declared_protostar_version_str

def get_declared_protostar_version(self):
return parse_protostar_version(self._declared_protostar_version_str)


class ProtostarVersionProviderDouble(ProtostarVersionProviderProtocol):
def __init__(self, protostar_version_str: str):
self._protostar_version_str = protostar_version_str

def get_protostar_version(self):
return parse_protostar_version(self._protostar_version_str)


@pytest.fixture(name="declared_protostar_version_provider")
def declared_protostar_version_provider_fixture(declared_protostar_version: str):
return DeclaredProtostarVersionProviderDouble(declared_protostar_version)


@pytest.fixture(name="protostar_version_provider")
def protostar_version_provider_fixture(protostar_version: str):
return ProtostarVersionProviderDouble(protostar_version)


@pytest.mark.parametrize(
"protostar_version, declared_protostar_version, is_compatible",
(
("0.1.2", "0.1.2", CompatibilityCheckResult.COMPATIBLE),
("0.1.2", "0.1.1", CompatibilityCheckResult.COMPATIBLE),
("1.0.0", "1.0.0", CompatibilityCheckResult.COMPATIBLE),
("0.1.1", "0.1.2", CompatibilityCheckResult.OUTDATED_PROTOSTAR),
("1.0.0", "1.1.0", CompatibilityCheckResult.OUTDATED_PROTOSTAR),
("0.2.0", "0.1.2", CompatibilityCheckResult.OUTDATED_DECLARED_VERSION),
("1.0.0", "0.9.0", CompatibilityCheckResult.OUTDATED_DECLARED_VERSION),
),
)
def test_compatibility(
declared_protostar_version_provider: DeclaredProtostarVersionProviderProtocol,
protostar_version_provider: ProtostarVersionProviderProtocol,
is_compatible: bool,
):
compatibility_checker = ProtostarCompatibilityWithProjectChecker(
protostar_version_provider,
declared_protostar_version_provider,
)

result = compatibility_checker.check_compatibility()

assert result == is_compatible

0 comments on commit d835da0

Please sign in to comment.