Skip to content

Commit

Permalink
Merge pull request #14 from evtn/dev
Browse files Browse the repository at this point in the history
1.1.7
  • Loading branch information
evtn authored Apr 26, 2023
2 parents 8730ab1 + 80236b3 commit d3279c4
Show file tree
Hide file tree
Showing 14 changed files with 502 additions and 66 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[report]
exclude_lines =
pragma: not covered
@overload
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,17 @@ print(tag[1]) # IndexError
print(tag[0]) # prints <a href="https://github.com/evtn/soda" />
```

Children can also be accessed directly through `tag.children` attribute.
~~Children can also be accessed directly through `tag.children` attribute.~~

This is not necessary for most tasks as of 1.1.6 with new methods and iterator protocol:

- `Tag.insert(int, Node)` inserts a node on an index. Be aware that `tag.children` is not flattened: `Tag.g("test", ["test1", "test2"]).insert(2, elem)` will insert `elem` *after* the array.
- `Tag.append(Node)` appends one node to the tag.
- `Tag.extend(*Node)` appends several nodes to the tag. *This is not the same as `.append([*Node])`*
- `Tag.pop(int?)` pops one node from specified index. If index is not provided, pops the last one.
- `Tag.iter_raw()` returns an iterable to get every Node of the tag. This doesn't dive into nested arrays, for that behaviour iterate over `Tag`
- You can also iterate over the `Tag` itself to get every flat node of it (no arrays)


## Fragments

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "soda-svg"
packages = [{include = "soda"}]
version = "1.1.5"
version = "1.1.7"
description = "Fast SVG generation tool"
authors = ["Dmitry Gritsenko <[email protected]>"]
license = "MIT"
Expand All @@ -14,6 +14,7 @@ keywords = ["soda", "svg", "xml"]
python = ">=3.7.0"

[tool.poetry.dev-dependencies]
pytest = "^7.3.1"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
8 changes: 6 additions & 2 deletions soda/custom_tags.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

from base64 import b64encode
from os import PathLike
from typing import BinaryIO
from .tags import Node, Tag
from pathlib import Path


class Root(Tag):
Expand Down Expand Up @@ -63,6 +65,8 @@ def from_file(file_object: BinaryIO, extension: str, **init_kwargs: bool) -> Ima
return Image(f"data:image/{extension};base64,{contents}", **init_kwargs)

@staticmethod
def from_filename(filename: str, extension: str, **init_kwargs: bool) -> Image:
with open(filename, "rb") as file:
def from_path(
pathlike: str | PathLike[str], extension: str, **init_kwargs: bool
) -> Image:
with Path(pathlike).open("rb") as file:
return Image.from_file(file, extension, **init_kwargs)
4 changes: 2 additions & 2 deletions soda/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ def horizontal(x: float = 0, *, relative: bool = False) -> str:

@staticmethod
def H(x: float) -> str:
return Path.vertical(x, relative=False)
return Path.horizontal(x, relative=False)

@staticmethod
def h(x: float) -> str:
return Path.vertical(x, relative=True)
return Path.horizontal(x, relative=True)

# Z
@staticmethod
Expand Down
34 changes: 11 additions & 23 deletions soda/point.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
from __future__ import annotations
from typing import Iterator, Sequence, Union, overload
from math import pi, cos, sin, hypot, acos
from math import cos, sin, hypot, acos, radians as degrees_to_radians

from .utils import eq
from .paths import Path, compact_path
from .tags import Node, Tag

PointLike = Union["Point", float, Sequence[float]]

rad_to_deg_k = 180 / pi


def radians_to_degrees(radians: float) -> float:
return radians * rad_to_deg_k


def degrees_to_radians(degrees: float) -> float:
return degrees / rad_to_deg_k


class Point:
def __init__(self, x: float = 0, y: float = 0):
Expand Down Expand Up @@ -151,22 +141,18 @@ def as_(self, x_argname: str = "x", y_argname: str = "y") -> dict[str, float]:

def angle(self, other: PointLike = (1, 0), center: PointLike = 0) -> float:
center = Point.from_(center)
other = Point.from_(other) - center
self = self - center
other = (Point.from_(other) - center).normalized()
self = (self - center).normalized()

dot_product = sum(self * other)
distance_product = self.distance() * other.distance()

if distance_product == 0:
return 0

return acos(dot_product / distance_product)
return acos(dot_product)

def normalized(self) -> Point:
return self / self.distance()

def __eq__(self, other: object) -> bool:
if not isinstance(other, (Point, float, list, tuple)):
if not isinstance(other, (Point, float, int, list, tuple)):
return False
other = Point.from_(other)
return eq((self - other).distance(), 0)
Expand Down Expand Up @@ -229,11 +215,11 @@ def horizontal(point: PointLike = 0, *, relative: bool = False) -> str:

@staticmethod
def H(point: PointLike = 0) -> str:
return PointPath.vertical(point, relative=False)
return PointPath.horizontal(point, relative=False)

@staticmethod
def h(point: PointLike = 0) -> str:
return PointPath.vertical(point, relative=True)
return PointPath.horizontal(point, relative=True)

# Z
@staticmethod
Expand Down Expand Up @@ -387,7 +373,9 @@ def polygon(*points: Point, **attributes: Node) -> Tag:
for i, point in enumerate(points)
]

return Tag.path(d=PointPath.build(*commands, PointPath.close()), **attributes)
return Tag.path(
d=PointPath.build(*commands, PointPath.close(), compact=True), **attributes
)

@staticmethod
def polyline(*points: Point, **attributes: Node) -> Tag:
Expand All @@ -396,4 +384,4 @@ def polyline(*points: Point, **attributes: Node) -> Tag:
for i, point in enumerate(points)
]

return Tag.path(d=PointPath.build(*commands), **attributes)
return Tag.path(d=PointPath.build(*commands, compact=True), **attributes)
94 changes: 78 additions & 16 deletions soda/tags.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from __future__ import annotations
from typing import Iterable, Optional, Union, overload
from typing import Iterable, Iterator, Optional, Sequence, Union, overload


Node = Union["Tag", str, float, "list[Node]"]


from .utils import escape, normalize_ident, trunc
FlatNode = Union["Tag", str, float]
Node = Union[FlatNode, "list[Node]"]


class MetaTag(type):
Expand Down Expand Up @@ -141,9 +138,31 @@ def __getitem__(

return self.get_attribute(item)

def insert(self, index: int, node: Node) -> None:
"""Inserts an entry into the tag"""
self.children.insert(index, node)

def append(self, child: Node) -> None:
"""A more list-like way to add a node to the tag"""
self(child)

def extend(self, children: Sequence[Node]) -> None:
"""A more list-like way to add several nodes to the tag"""
self(*children)

def pop(self, index: int = -1) -> Node:
"""Pop one (by default last one) entry from the tag"""
return self.children.pop(index)

def __iter__(self) -> Iterator[FlatNode]:
return iter(node_iterator(self.children))

def iter_raw(self) -> Iterator[Node]:
return iter(self.children)

def __call__(self, *children: Node, **attributes: Node) -> Tag:
if children:
self.children.extend(children)
self.children.extend(children)

if attributes:
for attr in attributes:
self[attr] = attributes[attr]
Expand All @@ -156,22 +175,64 @@ def __str__(self) -> str:
return self.render()

def build_child(
self, child: Node, tab_size: int = 0, tab_level: int = 0
self, child: FlatNode, tab_size: int = 0, tab_level: int = 0
) -> Iterable[str]:
if isinstance(child, (float, int)):
return self.build_child(str(trunc(child)), tab_size, tab_level)
yield from self.build_child(str(trunc(child)), tab_size, tab_level)
return

if isinstance(child, str):
yield " " * (tab_size * tab_level)
yield escape(child)

elif isinstance(child, list):
for subchild in child:
yield from self.build_child(subchild, tab_size, tab_level)

else:
yield from child.build(tab_size, tab_level)

def compare_attrs(self, other: Tag) -> bool:
attrs1 = self.attributes
attrs2 = other.attributes

if attrs1.keys() ^ attrs2.keys():
return False

for key in attrs1:
if attrs1[key] != attrs2[key]:
return False

return True

def compare_children(self, other: Tag) -> bool:
iter_self = iter(self)
iter_other = iter(other)

# be aware that we can't compare length here because possible
# node nesting prevents us from doing so

for self_child in iter_self:
other_child = next(iter_other, None)

if other_child is None:
return False

if self_child != other_child:
return False

# remaining elements in `other`
if next(iter_other, None) is not None:
return False

return True

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

return (
(self.tag_name == other.tag_name)
and self.compare_attrs(other)
and self.compare_children(other)
)

def build(self, tab_size: int = 0, tab_level: int = 0) -> Iterable[str]:
tag_name = self.tag_name
pretty = bool(tab_size)
Expand Down Expand Up @@ -212,7 +273,7 @@ def build(self, tab_size: int = 0, tab_level: int = 0) -> Iterable[str]:
if self.children or not self.self_closing:
yield self.brackets[2] # >

for child in self.children:
for child in self:
yield separator
yield from self.build_child(child, tab_size, tab_level + 1)

Expand Down Expand Up @@ -276,7 +337,7 @@ def __init__(self, *children: Node):
super().__init__("soda:fragment", *children)

def build(self, tab_size: int = 0, tab_level: int = 0) -> Iterable[str]:
for child in self.children:
for child in self:
yield from self.build_child(child, tab_size, tab_level)

def render(self, pretty: bool = False, tab_size: int = 2) -> str:
Expand All @@ -288,3 +349,4 @@ def copy(self) -> Fragment:


from .xml_parse import xml_to_tag
from .utils import escape, node_iterator, normalize_ident, trunc
16 changes: 13 additions & 3 deletions soda/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Callable, Iterable

from .tags import Node
from .tags import FlatNode, Fragment, Node
from .config_mod import config

char_range: Callable[[str, str], "map[str]"] = lambda s, e: map(
Expand Down Expand Up @@ -42,8 +42,6 @@ def normalize_ident_gen(attr: str) -> Iterable[str]:
if c == "_":
if started:
skipped_underscores += 1
elif not config.strip_underscores:
yield "_"
continue
else:
started = True
Expand All @@ -59,6 +57,8 @@ def normalize_ident_gen(attr: str) -> Iterable[str]:

def trunc(value: Node) -> Node:
if isinstance(value, float):
if value.is_integer():
return int(value)
return round(value, config.decimal_length)
return value

Expand All @@ -67,3 +67,13 @@ def eq(v1: float, v2: float) -> bool:
eps: float = 10 ** -(2 * config.decimal_length)

return abs(v1 - v2) < eps


def node_iterator(iterable: Iterable[Node]) -> Iterable[FlatNode]:
for elem in iterable:
if isinstance(elem, list):
yield from node_iterator(elem)
elif isinstance(elem, Fragment):
yield from elem
else:
yield elem
Loading

0 comments on commit d3279c4

Please sign in to comment.