Skip to content

Commit

Permalink
Merge branch 'master' of github.com:vedro-universe/vedro into fixed-seed
Browse files Browse the repository at this point in the history
  • Loading branch information
tsv1 committed Oct 21, 2023
2 parents a3141b4 + 605ca06 commit f7031ad
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
- name: Build
run: make build
- name: Publish
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- name: Checkout
Expand Down
17 changes: 9 additions & 8 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
asynctest==0.13.0
baby-steps==1.2.3
baby-steps==1.3.0
bump2version==1.0.1
codecov==2.1.13
coverage==7.2.5
flake8==5.0.4
isort==5.11.5
mypy==1.3.0
pytest==7.3.1
pytest-asyncio==0.21.0
pytest-cov==4.0.0
coverage==7.3.2
flake8==6.1.0
isort==5.12.0
mypy==1.6.1
pytest==7.4.2
pytest-asyncio==0.21.1
pytest-cov==4.1.0
setuptools==68.1.2
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def find_dev_required():
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development",
"Typing :: Typed",
],
Expand Down
4 changes: 2 additions & 2 deletions tests/core/test_virtual_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_virtual_scenario_unique_id(*, scenario_: Type[Scenario]):
unique_id = virtual_scenario.unique_id

with then:
assert unique_id == "c2NlbmFyaW9zL3NjZW5hcmlvLnB5OjpTY2VuYXJpbw"
assert unique_id == "scenarios/scenario.py::Scenario"


def test_virtual_scenario_unique_hash(*, scenario_: Type[Scenario]):
Expand All @@ -80,7 +80,7 @@ def test_virtual_template_unique_id(*, template_: Type[Scenario]):
unique_id = virtual_scenario.unique_id

with then:
assert unique_id == "c2NlbmFyaW9zL3NjZW5hcmlvLnB5OjpTY2VuYXJpb18wX1ZlZHJvU2NlbmFyaW8jMA"
assert unique_id == "scenarios/scenario.py::Scenario_0_VedroScenario#0"


def test_virtual_scenario_path(*, scenario_: Type[Scenario]):
Expand Down
14 changes: 12 additions & 2 deletions tests/plugins/tagger/test_logic_tag_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ def test_not_match():


def test_invalid_expr():
with given:
matcher = LogicTagMatcher("P0 and")

with when, raises(ValueError) as exc:
LogicTagMatcher("P0 and")
matcher.match(set())

with then:
assert exc.type is ValueError
assert str(exc.value) == ("Invalid tag expr 'P0 and'. "
"Error: Expected end of text, found 'and' "
"(at char 3), (line:1, col:4)")


@pytest.mark.parametrize("expr", [
Expand All @@ -43,8 +49,12 @@ def test_invalid_expr():
"not",
])
def test_invalid_tag(expr: str):
with given:
matcher = LogicTagMatcher(expr)

with when, raises(ValueError) as exc:
LogicTagMatcher(expr)
matcher.match(set())

with then:
assert exc.type is ValueError
assert str(exc.value).startswith(f"Invalid tag expr {expr!r}. Error: ")
1 change: 1 addition & 0 deletions tests/plugins/tagger/test_tag_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ def test_tag_matcher():

with then:
assert exc.type is TypeError
assert str(exc.value).startswith("Can't instantiate abstract class")
30 changes: 25 additions & 5 deletions tests/plugins/tagger/test_tagger_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,8 @@ async def test_tags_type_validation(*, dispatcher: Dispatcher):

with then:
assert exc.type is TypeError
assert str(exc.value) == (
f"Scenario '{scenario.rel_path}' tags must be a list, tuple or set, got <class 'dict'>"
)
assert str(exc.value) == (f"Scenario '{scenario.unique_id}' tags must be a list, "
"tuple or set, got <class 'dict'>")


@pytest.mark.asyncio
Expand All @@ -209,6 +208,27 @@ async def test_tags_value_validation(*, dispatcher: Dispatcher):

with then:
assert exc.type is ValueError
assert str(exc.value) == (
f"Scenario '{scenario.rel_path}' tag '-SMOKE' is not valid"
assert str(exc.value).startswith(
f"Scenario '{scenario.unique_id}' tag '-SMOKE' is not valid"
)


@pytest.mark.asyncio
@pytest.mark.usefixtures(tagger.__name__)
async def test_tags_tag_type_validation(*, dispatcher: Dispatcher):
with given:
await fire_arg_parsed_event(dispatcher, tags="SMOKE")

scenario = make_vscenario(tags=[None]) # type: ignore
scheduler = Scheduler([scenario])

startup_event = StartupEvent(scheduler)

with when, raises(BaseException) as exc:
await dispatcher.fire(startup_event)

with then:
assert exc.type is ValueError
assert str(exc.value).startswith(
f"Scenario '{scenario.unique_id}' tag 'None' is not valid"
)
2 changes: 1 addition & 1 deletion vedro/commands/_cmd_arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class CommandArgumentParser(ArgumentParser):
def parse_known_args(self, args: Optional[Sequence[str]] = None,
def parse_known_args(self, args: Optional[Sequence[str]] = None, # type: ignore
namespace: Optional[Namespace] = None) -> Tuple[Namespace, List[str]]:
if args is None:
# $ prog command <...>
Expand Down
12 changes: 4 additions & 8 deletions vedro/core/_virtual_scenario.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
from base64 import b64encode
from hashlib import blake2b
from inspect import BoundArguments
from pathlib import Path
Expand Down Expand Up @@ -29,17 +28,14 @@ def steps(self) -> List[VirtualStep]:

@property
def unique_id(self) -> str:
unique_name = f"{self.rel_path}::{self.name}"
unique_id = f"{self.rel_path}::{self.name}"
if self.template_index is not None:
unique_name += f"#{self.template_index}"
return b64encode(unique_name.encode()).decode().strip("=")
unique_id += f"#{self.template_index}"
return unique_id

@property
def unique_hash(self) -> str:
unique_name = f"{self.rel_path}::{self.name}"
if self.template_index is not None:
unique_name += f"#{self.template_index}"
return blake2b(unique_name.encode(), digest_size=20).hexdigest()
return blake2b(self.unique_id.encode(), digest_size=20).hexdigest()

@property
def template_index(self) -> Union[int, None]:
Expand Down
8 changes: 8 additions & 0 deletions vedro/plugins/skipper/_skipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def __init__(self, config: Type["Skipper"]) -> None:
self._subject: Union[str, None] = None
self._selected: List[_CompositePath] = []
self._deselected: List[_CompositePath] = []
self._forbid_only = config.forbid_only

def subscribe(self, dispatcher: Dispatcher) -> None:
dispatcher.listen(ArgParseEvent, self.on_arg_parse) \
Expand Down Expand Up @@ -63,6 +64,7 @@ def _normalize_path(self, file_or_dir: str) -> str:
path = os.path.normpath(file_or_dir)
if os.path.isabs(path):
return path
# Joining "./scenarios" will be removed in v2
if os.path.commonpath(["scenarios", path]) != "scenarios":
path = os.path.join("scenarios", path)
return os.path.abspath(path)
Expand Down Expand Up @@ -138,6 +140,9 @@ async def on_startup(self, event: StartupEvent) -> None:
if self._is_scenario_skipped(scenario):
scenario.skip(reason=self._get_skip_reason(scenario))
if self._is_scenario_special(scenario):
if self._forbid_only:
raise ValueError(f"Scenario '{scenario.unique_id}' has @vedro.only, but "
"'forbid_only' option is enabled")
special_scenarios.add(scenario.unique_id)

if len(special_scenarios) > 0:
Expand All @@ -150,3 +155,6 @@ class Skipper(PluginConfig):
plugin = SkipperPlugin
description = "Allows selective scenario skipping and selection " \
"based on file/directory or subject"

# Forbid execution of scenarios with '@vedro.only' decorator
forbid_only: bool = False
34 changes: 19 additions & 15 deletions vedro/plugins/tagger/_tagger.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from enum import Enum
from typing import Any, Callable, Type, Union

from vedro.core import Dispatcher, Plugin, PluginConfig, VirtualScenario
Expand Down Expand Up @@ -29,26 +30,29 @@ def on_arg_parse(self, event: ArgParseEvent) -> None:
def on_arg_parsed(self, event: ArgParsedEvent) -> None:
self._tags_expr = event.args.tags

def _get_tags(self, scenario: VirtualScenario) -> Any:
tags = getattr(scenario._orig_scenario, "tags", ())
if not isinstance(tags, (list, tuple, set)):
raise TypeError(f"Scenario '{scenario.rel_path}' tags must be a list, tuple or set, "
f"got {type(tags)}")
def _get_tags(self, scenario: VirtualScenario, validate: Callable[[str], bool]) -> Any:
orig_tags = getattr(scenario._orig_scenario, "tags", ())
if not isinstance(orig_tags, (list, tuple, set)):
raise TypeError(f"Scenario '{scenario.unique_id}' tags must be a list, tuple or set, "
f"got {type(orig_tags)}")
tags = []
for tag in orig_tags:
if isinstance(tag, Enum):
tag = tag.value
try:
validate(tag)
except Exception as e:
raise ValueError(f"Scenario '{scenario.unique_id}' tag '{tag}' is not valid ({e})")
else:
tags.append(tag)
return tags

async def on_startup(self, event: StartupEvent) -> None:
if self._tags_expr is None:
return
self._matcher = self._matcher_factory(str(self._tags_expr))

self._matcher = self._matcher_factory(self._tags_expr)
async for scenario in event.scheduler:
tags = self._get_tags(scenario)

for tag in tags:
if not self._matcher.validate(tag):
raise ValueError(f"Scenario '{scenario.rel_path}' tag '{tag}' is not valid")

if not self._matcher.match(set(tags)):
tags = self._get_tags(scenario, self._matcher.validate)
if self._tags_expr and not self._matcher.match(set(tags)):
event.scheduler.ignore(scenario)


Expand Down
15 changes: 12 additions & 3 deletions vedro/plugins/tagger/logic_tag_matcher/_logic_tag_matcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from re import fullmatch
from typing import Set, cast
from typing import Set, Union, cast

from pyparsing import Literal
from pyparsing import ParserElement as Parser
Expand All @@ -23,13 +23,22 @@ def __init__(self, expr: str) -> None:
(Literal("and"), 2, opAssoc.LEFT, self._create_and),
(Literal("or"), 2, opAssoc.LEFT, self._create_or),
])
self._grammar = self._parse(self._parser, expr)
self._grammar: Union[Expr, None] = None

def match(self, tags: Set[str]) -> bool:
if self._grammar is None:
self._grammar = self._parse(self._parser, self._expr)
return self._grammar(tags)

def validate(self, tag: str) -> bool:
return fullmatch(self.tag_pattern, tag) is not None
if not isinstance(tag, str):
raise TypeError(f"Tag must be a str, got {type(tag)}")

res = fullmatch(self.tag_pattern, tag)
if res is None:
raise ValueError(f"Tag must match regex {self.tag_pattern!r}")

return True

def _create_tag(self, orig: str, location: int, tokens: ParseResults) -> Expr:
tag = tokens[0]
Expand Down

0 comments on commit f7031ad

Please sign in to comment.