Skip to content

Commit

Permalink
feat: markers and tests
Browse files Browse the repository at this point in the history
Signed-off-by: Frost Ming <[email protected]>
  • Loading branch information
frostming committed Nov 21, 2023
1 parent 825c203 commit 2342702
Show file tree
Hide file tree
Showing 30 changed files with 3,198 additions and 311 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@ on:

jobs:
Testing:
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
- name: Set up PDM
uses: pdm-project/setup-pdm@v3
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.arch }}
cache: "true"

- name: Install packages
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# pkg-logical
# Dep-Logic

Logical operational specifiers and markers.
Python dependency specifications supporting logical operations
2 changes: 1 addition & 1 deletion pdm.lock

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

11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
[project]
name = "pkg-logical"
version = "0.1.0"
description = "Logical operational specifiers and markers"
name = "dep-logic"
description = "Python dependency specifications supporting logical operations"
authors = [
{name = "Frost Ming", email = "[email protected]"},
]
dependencies = [
"packaging>=22",
]
requires-python = ">=3.10"
requires-python = ">=3.8"
readme = "README.md"
license = {text = "Apache-2.0"}
dynamic = ["version"]

[tool.pdm.version]
source = "scm"

[build-system]
requires = ["pdm-backend"]
Expand Down
File renamed without changes.
101 changes: 101 additions & 0 deletions src/dep_logic/markers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Adapted from poetry/core/version/markers.py
# The original work is published under the MIT license.
# Copyright (c) 2020 Sébastien Eustace
# Adapted by Frost Ming (c) 2023

from __future__ import annotations

import functools
from typing import TYPE_CHECKING

from packaging.markers import InvalidMarker as _InvalidMarker
from packaging.markers import Marker as _Marker

from dep_logic.markers.any import AnyMarker
from dep_logic.markers.base import BaseMarker
from dep_logic.markers.empty import EmptyMarker
from dep_logic.markers.multi import MultiMarker
from dep_logic.markers.single import MarkerExpression
from dep_logic.markers.union import MarkerUnion
from dep_logic.utils import get_reflect_op

if TYPE_CHECKING:
from typing import List, Literal, Tuple, Union

from packaging.markers import Op, Value, Variable

_ParsedMarker = Tuple[Variable, Op, Value]
_ParsedMarkers = Union[
_ParsedMarker, List[Union["_ParsedMarkers", Literal["or", "and"]]]
]


__all__ = [
"parse_marker",
"from_pkg_marker",
"InvalidMarker",
"BaseMarker",
"AnyMarker",
"EmptyMarker",
"MarkerExpression",
"MarkerUnion",
"MultiMarker",
]


class InvalidMarker(ValueError):
"""
An invalid marker was found, users should refer to PEP 508.
"""


@functools.lru_cache(maxsize=None)
def parse_marker(marker: str) -> BaseMarker:
if marker == "<empty>":
return EmptyMarker()

if not marker or marker == "*":
return AnyMarker()
try:
parsed = _Marker(marker)
except _InvalidMarker as e:
raise InvalidMarker(str(e)) from e

markers = _build_markers(parsed._markers)

return markers


def from_pkg_marker(marker: _Marker) -> BaseMarker:
return _build_markers(marker._markers)


def _build_markers(markers: _ParsedMarkers) -> BaseMarker:
from packaging.markers import Variable

if isinstance(markers, tuple):
if isinstance(markers[0], Variable):
name, op, value, reversed = (
str(markers[0]),
str(markers[1]),
str(markers[2]),
False,
)
else:
# in reverse order
name, op, value, reversed = (
str(markers[2]),
get_reflect_op(str(markers[1])),
str(markers[0]),
True,
)
return MarkerExpression(name, op, value, reversed)
or_groups: list[BaseMarker] = [AnyMarker()]
for item in markers:
if item == "or":
or_groups.append(AnyMarker())
elif item == "and":
continue
else:
or_groups[-1] &= _build_markers(item)
return MarkerUnion.of(*or_groups)
43 changes: 43 additions & 0 deletions src/dep_logic/markers/any.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from dep_logic.markers.base import BaseMarker


class AnyMarker(BaseMarker):
def __and__(self, other: BaseMarker) -> BaseMarker:
return other

__rand__ = __and__

def __or__(self, other: BaseMarker) -> BaseMarker:
return self

__ror__ = __or__

def is_any(self) -> bool:
return True

def evaluate(self, environment: dict[str, str] | None = None) -> bool:
return True

def without_extras(self) -> BaseMarker:
return self

def exclude(self, marker_name: str) -> BaseMarker:
return self

def only(self, *marker_names: str) -> BaseMarker:
return self

def __str__(self) -> str:
return ""

def __repr__(self) -> str:
return "<AnyMarker>"

def __hash__(self) -> int:
return hash("any")

def __eq__(self, other: object) -> bool:
if not isinstance(other, BaseMarker):
return NotImplemented

return isinstance(other, AnyMarker)
59 changes: 59 additions & 0 deletions src/dep_logic/markers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import Any


class BaseMarker(metaclass=ABCMeta):
@property
def complexity(self) -> tuple[int, int]:
"""
The first number is the number of marker expressions,
and the second number is 1 if the marker is single-like.
"""
return 1, 1

@abstractmethod
def __and__(self, other: Any) -> BaseMarker:
raise NotImplementedError

@abstractmethod
def __or__(self, other: Any) -> BaseMarker:
raise NotImplementedError

def is_any(self) -> bool:
return False

def is_empty(self) -> bool:
return False

@abstractmethod
def evaluate(self, environment: dict[str, str] | None = None) -> bool:
raise NotImplementedError

@abstractmethod
def without_extras(self) -> BaseMarker:
raise NotImplementedError

@abstractmethod
def exclude(self, marker_name: str) -> BaseMarker:
raise NotImplementedError

@abstractmethod
def only(self, *marker_names: str) -> BaseMarker:
raise NotImplementedError

def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self}>"

@abstractmethod
def __str__(self) -> str:
raise NotImplementedError

@abstractmethod
def __hash__(self) -> int:
raise NotImplementedError

@abstractmethod
def __eq__(self, other: object) -> bool:
raise NotImplementedError
43 changes: 43 additions & 0 deletions src/dep_logic/markers/empty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from dep_logic.markers.base import BaseMarker


class EmptyMarker(BaseMarker):
def __and__(self, other: BaseMarker) -> BaseMarker:
return self

__rand__ = __and__

def __or__(self, other: BaseMarker) -> BaseMarker:
return other

__ror__ = __or__

def is_empty(self) -> bool:
return True

def evaluate(self, environment: dict[str, str] | None = None) -> bool:
return False

def without_extras(self) -> BaseMarker:
return self

def exclude(self, marker_name: str) -> BaseMarker:
return self

def only(self, *marker_names: str) -> BaseMarker:
return self

def __str__(self) -> str:
return "<empty>"

def __repr__(self) -> str:
return "<EmptyMarker>"

def __hash__(self) -> int:
return hash("empty")

def __eq__(self, other: object) -> bool:
if not isinstance(other, BaseMarker):
return NotImplemented

return isinstance(other, EmptyMarker)
Loading

0 comments on commit 2342702

Please sign in to comment.