internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: bokeh/bokeh

Issue:
bokehjs' version has duplicated dev suffix
```sh
$ jq '.version' bokehjs/package.json
"2.2.0dev4-dev.4"
```
Should be `2.2.0-dev.4` instead.


Discussion:

Resolving patch:
diff --git a/release/config.py b/release/config.py
--- a/release/config.py
+++ b/release/config.py
@@ -71,7 +71,7 @@ def version_type(self) -> VersionType:
     def js_version(self) -> str:
         if self.ext is None:
             return self.version
-        return f"{self.version}-{self.ext_type}.{self.ext_number}"
+        return f"{self.base_version}-{self.ext_type}.{self.ext_number}"
 
     @property
     def release_level(self) -> str:


Test patch:
Repository: bokeh/bokeh

Issue:
[BUG] Link in docs is not working for fill color property
https://docs.bokeh.org/en/latest/_modules/bokeh/core/property_mixins.html#FillProps


Discussion:

Resolving patch:
diff --git a/bokeh/core/property_mixins.py b/bokeh/core/property_mixins.py
--- a/bokeh/core/property_mixins.py
+++ b/bokeh/core/property_mixins.py
@@ -118,7 +118,7 @@ class SomeGlyph(Glyph):
 - a 3-tuple of integers (r,g,b) between 0 and 255
 - a 4-tuple of (r,g,b,a) where r,g,b are integers between 0..255 and a is between 0..1
 
-.. _CSS colors: http://www.w3schools.com/cssref/css_colornames.asp
+.. _CSS colors: https://www.w3schools.com/colors/colors_names.asp
 
 """
 


Test patch:
Repository: bokeh/bokeh

Issue:
[FEATURE] Allow defining custom format for NaNs in DataTable NumberFormatter
Currently the DataTable NumberFormatter always renders NaNs and Nones as 'NaN' because that's what Numbro.js does. This is rarely desirable formatting so it would be nice to provide an option to override this.


Discussion:

Resolving patch:
diff --git a/bokeh/models/widgets/tables.py b/bokeh/models/widgets/tables.py
--- a/bokeh/models/widgets/tables.py
+++ b/bokeh/models/widgets/tables.py
@@ -227,6 +227,10 @@ class NumberFormatter(StringFormatter):
     The language to use for formatting language-specific features (e.g. thousands separator).
     """)
 
+    nan_format = String(help="""
+    Formatting to apply to NaN and None values (falls back to Numbro formatting if not set).
+    """)
+
     rounding = Enum(RoundingFunction, help="""
     Rounding functions (round, floor, ceil) and their synonyms (nearest, rounddown, roundup).
     """)


Test patch:
diff --git a/bokehjs/test/unit/models/widgets/tables/cell_formatters.ts b/bokehjs/test/unit/models/widgets/tables/cell_formatters.ts
--- a/bokehjs/test/unit/models/widgets/tables/cell_formatters.ts
+++ b/bokehjs/test/unit/models/widgets/tables/cell_formatters.ts
@@ -1,6 +1,6 @@
 import {expect} from "assertions"
 
-import {DateFormatter} from "@bokehjs/models/widgets/tables/cell_formatters"
+import {DateFormatter, NumberFormatter} from "@bokehjs/models/widgets/tables/cell_formatters"
 
 describe("cell_formatters module", () => {
 
@@ -74,4 +74,20 @@ describe("cell_formatters module", () => {
       })
     })
   })
+
+  describe("NumberFormatter", () => {
+
+    describe("doFormat method", () => {
+
+      it("should apply nan_format to null", () => {
+        const df = new NumberFormatter({nan_format: "-"})
+        expect(df.doFormat(0, 0, null, {}, {})).to.be.equal('<div class="bk" style="text-align: left;">-</div>')
+      })
+
+      it("should apply nan_format to nan", () => {
+        const df = new NumberFormatter({nan_format: "-"})
+        expect(df.doFormat(0, 0, NaN, {}, {})).to.be.equal('<div class="bk" style="text-align: left;">-</div>')
+      })
+    })
+  })
 })
Repository: bokeh/bokeh

Issue:
Color site for OpenURL no longer exists

ref: https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#openurl

The `http://www.colors.commutercreative.com` site used for the `OpenURL` callback in the example above seems to no longer exist. 

Could probably use e.g. `http://www.html-color-names.com/firebrick.php` instead.
Color site for OpenURL no longer exists

ref: https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#openurl

The `http://www.colors.commutercreative.com` site used for the `OpenURL` callback in the example above seems to no longer exist. 

Could probably use e.g. `http://www.html-color-names.com/firebrick.php` instead.


Discussion:
Indeed it doesn't look like the currently referenced page exists at all on the original site anymore. 

May I help with this change? 
@morrme certainly, a PR to update would be very welcome
Indeed it doesn't look like the currently referenced page exists at all on the original site anymore. 

May I help with this change? 
@morrme certainly, a PR to update would be very welcome

Resolving patch:
diff --git a/sphinx/source/docs/user_guide/examples/interaction_open_url.py b/sphinx/source/docs/user_guide/examples/interaction_open_url.py
--- a/sphinx/source/docs/user_guide/examples/interaction_open_url.py
+++ b/sphinx/source/docs/user_guide/examples/interaction_open_url.py
@@ -17,7 +17,7 @@
 # use the "color" column of the CDS to complete the URL
 # e.g. if the glyph at index 10 is selected, then @color
 # will be replaced with source.data['color'][10]
-url = "http://www.colors.commutercreative.com/@color/"
+url = "http://www.html-color-names.com/@color.php"
 taptool = p.select(type=TapTool)
 taptool.callback = OpenURL(url=url)
 


Test patch:
Repository: abey79/vpype

Issue:
Bug: non-empty SVG should never have a width and/or height of 0
According to the spec, a width/height of 0 means everything should be hidden (see meerk40t/svgelements#68). Thin dimension for thin SVG should be capped at 1px.

Relates to #79 


Discussion:

Resolving patch:
diff --git a/vpype/io.py b/vpype/io.py
--- a/vpype/io.py
+++ b/vpype/io.py
@@ -340,7 +340,8 @@ def write_svg(
     """Create a SVG from a :py:class:`Document` instance.
 
     If no page size is provided (or (0, 0) is passed), the SVG generated has bounds tightly
-    fitted around the geometries. Otherwise the provided size (in pixel) is used.
+    fitted around the geometries. Otherwise the provided size (in pixel) is used. The width
+    and height is capped to a minimum of 1 pixel.
 
     By default, no translation is applied on the geometry. If `center=True`, geometries are
     moved to the center of the page.
@@ -393,13 +394,14 @@ def write_svg(
     else:
         corrected_doc = document
 
-    # output SVG
-    size_cm = tuple(f"{round(s / UNITS['cm'], 8)}cm" for s in size)
+    # output SVG, width/height are capped to 1px
+    capped_size = tuple(max(1, s) for s in size)
+    size_cm = tuple(f"{round(s / UNITS['cm'], 8)}cm" for s in capped_size)
     dwg = svgwrite.Drawing(size=size_cm, profile="tiny", debug=False)
     inkscape = Inkscape(dwg)
     dwg.attribs.update(
         {
-            "viewBox": f"0 0 {size[0]} {size[1]}",
+            "viewBox": f"0 0 {capped_size[0]} {capped_size[1]}",
             "xmlns:dc": "http://purl.org/dc/elements/1.1/",
             "xmlns:cc": "http://creativecommons.org/ns#",
             "xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",


Test patch:
Repository: abey79/vpype

Issue:
Test with python 3.9 and update CI accordingly
Note: after cursory testing, it seams that the current installation procedure does not work for Python 3.9.0 on Windows


Discussion:

Resolving patch:
diff --git a/vpype_cli/operations.py b/vpype_cli/operations.py
--- a/vpype_cli/operations.py
+++ b/vpype_cli/operations.py
@@ -361,8 +361,43 @@ def pagesize(document: vp.Document, size, landscape) -> vp.Document:
     return document
 
 
+LAYOUT_HELP = f"""Layout the geometries on the provided page size.
+
+SIZE may be one of:
+
+    {', '.join(vp.PAGE_SIZES.keys())}
+
+Alternatively, a custom size can be specified in the form of WIDTHxHEIGHT. WIDTH and
+HEIGHT may include units. If only one has an unit, the other is assumed to have the
+same unit. If none have units, both are assumed to be pixels by default. Here are some
+examples:
+
+\b
+    --page-size 11x14in     # 11in by 14in
+    --page-size 1024x768    # 1024px by 768px
+    --page-size 13.5inx4cm  # 13.5in by 4cm
+
+Portrait orientation is enforced, unless `--landscape` is used, in which case landscape
+orientation is enforced.
+
+By default, this command centers everything on the page. The horizontal and vertical
+alignment can be adjusted using the `--align`, resp. `--valign` options.
+
+Optionally, this command can scale the geometries to fit specified margins with the
+`--fit-to-margins` option.
+
+Examples:
+
+\b
+    Fit the geometries to 3cm margins with top alignment (a generally
+    pleasing arrangement for square designs on portrait-oriented pages):
+
+        vpype read input.svg layout --fit-to-margins 3cm --valign top a4 write.svg
+"""
+
+
 # noinspection PyShadowingNames
-@cli.command(group="Operations")
+@cli.command(group="Operations", help=LAYOUT_HELP)
 @click.argument("size", type=vp.PageSizeType(), required=True)
 @click.option("-l", "--landscape", is_flag=True, default=False, help="Landscape orientation.")
 @click.option(
@@ -395,21 +430,7 @@ def layout(
     align: str,
     valign: str,
 ) -> vp.Document:
-    """Layout the geometries on the provided page size.
-
-    By default, this command centers everything on the page. The horizontal and vertical
-    alignment can be adjusted using the `--align`, resp. `--valign` options.
-
-    Optionally, this command can scale the geometries to fit specified margins with the
-    `--fit-to-margins` option.
-
-    Examples:
-
-        Fit the geometries to 3cm margins with top alignment (a generally pleasing arrangement
-        for square designs on portrait-oriented pages):
-
-            vpype read input.svg layout --fit-to-margins 3cm --valign top a4 write.svg
-    """
+    """Layout command"""
 
     size = _normalize_page_size(size, landscape)
 


Test patch:
Repository: abey79/vpype

Issue:
Doesn't have support for outputting a2 to a0 paper sizes
I was hoping to output some large paper sizes to plot.

the error messages were the same for paper sizes a2 to a0

```
Traceback (most recent call last):
  File "/usr/local/bin/vpype", line 8, in <module>
    sys.exit(cli())
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/vpype_cli/cli.py", line 74, in main
    return super().main(args=preprocess_argument_list(args), **extra)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1290, in invoke
    return _process_result(rv)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1224, in _process_result
    value = ctx.invoke(self.result_callback, value, **ctx.params)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/vpype_cli/cli.py", line 128, in process_pipeline
    execute_processors(processors)
  File "/usr/local/lib/python3.8/site-packages/vpype_cli/cli.py", line 212, in execute_processors
    state = proc(state)
  File "/usr/local/lib/python3.8/site-packages/vpype/decorators.py", line 150, in global_processor
    state.document = f(state.document, *args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/vpype_cli/write.py", line 199, in write
    page_size_px = convert_page_size(page_size)
  File "/usr/local/lib/python3.8/site-packages/vpype/utils.py", line 154, in convert_page_size
    raise ValueError(f"page size '{value}' unknown")
ValueError: page size 'a2' unknown
```


Discussion:
@JoshuaMorris indeed for some reason I haven't added the larger page sizes. You can work around this by using the physical dimensions instead: `vpype ... write --page-size 420x594mm ...`(for A2).

Resolving patch:
diff --git a/vpype/utils.py b/vpype/utils.py
--- a/vpype/utils.py
+++ b/vpype/utils.py
@@ -53,6 +53,9 @@ def _mm_to_px(x: float, y: float) -> Tuple[float, float]:
     "a5": _mm_to_px(148.0, 210.0),
     "a4": _mm_to_px(210.0, 297.0),
     "a3": _mm_to_px(297.0, 420.0),
+    "a2": _mm_to_px(420.0, 594.0),
+    "a1": _mm_to_px(594.0, 841.0),
+    "a0": _mm_to_px(841.0, 1189.0),
     "letter": _mm_to_px(215.9, 279.4),
     "legal": _mm_to_px(215.9, 355.6),
     "executive": _mm_to_px(185.15, 266.7),


Test patch:
Repository: abey79/vpype

Issue:
viewer: keep "fit to viewport" until manually zoomed/panned
Once in "fit to viewport" mode (default at start and when clicked, it should stick to that mode upon resize, until manually zoomed/panned


Discussion:

Resolving patch:
diff --git a/vpype_viewer/engine.py b/vpype_viewer/engine.py
--- a/vpype_viewer/engine.py
+++ b/vpype_viewer/engine.py
@@ -93,6 +93,8 @@ def __init__(
         self._layer_painters: Dict[int, List[Painter]] = defaultdict(list)
         self._paper_bounds_painter: Optional[PaperBoundsPainter] = None
 
+        self._fit_to_viewport_flag = True
+
     def post_init(self, ctx: mgl.Context, width: int = 100, height: int = 100):
         """Post-init configuration to provide a GL context."""
 
@@ -239,6 +241,8 @@ def fit_to_viewport(self):
             y1 - (self._viewport_height / self._scale - h) / 2,
         ]
 
+        self._fit_to_viewport_flag = True
+
         self._update(False)
 
     def viewport_to_model(self, x: float, y: float) -> Tuple[float, float]:
@@ -251,8 +255,11 @@ def resize(self, width: int, height: int) -> None:
             width: new viewport width
             height: new viewport height
         """
-        self._viewport_width = width
-        self._viewport_height = height
+        self._viewport_width = max(width, 1)
+        self._viewport_height = max(height, 1)
+
+        if self._fit_to_viewport_flag:
+            self.fit_to_viewport()
 
     def _get_projection(self) -> np.ndarray:
         proj = orthogonal_projection_matrix(
@@ -274,6 +281,10 @@ def pan(self, dx: float, dy: float) -> None:
             dx: horizontal distance to pan
             dy: vertical distance to pan
         """
+
+        if dx != 0 or dy != 0:
+            self._fit_to_viewport_flag = False
+
         self._origin = (self._origin[0] - dx / self._scale, self._origin[1] - dy / self._scale)
         self._update(False)
 
@@ -288,6 +299,9 @@ def zoom(self, delta_zoom: float, mouse_x: float, mouse_y: float) -> None:
             mouse_x: mouse X coordinate
             mouse_y: mouse Y coordinate
         """
+        if delta_zoom != 0:
+            self._fit_to_viewport_flag = False
+
         new_scale = self._scale * (1 + delta_zoom)
         new_scale = max(min(new_scale, 100000), 0.05)  # clamp to reasonable values
 
diff --git a/vpype_viewer/qtviewer/viewer.py b/vpype_viewer/qtviewer/viewer.py
--- a/vpype_viewer/qtviewer/viewer.py
+++ b/vpype_viewer/qtviewer/viewer.py
@@ -67,9 +67,6 @@ def __init__(self, document: Optional[vp.Document] = None, parent=None):
             view_mode=ViewMode.OUTLINE, show_pen_up=False, render_cb=self.update
         )
 
-        # adjust default scale based on hidpi
-        self.engine.scale = self.window().devicePixelRatio()
-
     def document(self) -> Optional[vp.Document]:
         """Return the :class:`vpype.Document` currently assigned to the widget."""
         return self._document


Test patch:
Repository: abey79/vpype

Issue:
Windows installer not working v 1.6.0
The windows installer for Vpype 1.6.0 does not seem to be working with Vpype v 1.6.0. Previous version 1.5.1 was working fine. On upgrading to 1.6.0 the installed Vpype is not working. Attached error message
[Vpype.errror.txt](https://github.com/abey79/vpype/files/6518264/Vpype.errror.txt)

Pip install on Windows is working fine.
Windows installer not working v 1.6.0
The windows installer for Vpype 1.6.0 does not seem to be working with Vpype v 1.6.0. Previous version 1.5.1 was working fine. On upgrading to 1.6.0 the installed Vpype is not working. Attached error message
[Vpype.errror.txt](https://github.com/abey79/vpype/files/6518264/Vpype.errror.txt)

Pip install on Windows is working fine.


Discussion:

Resolving patch:
diff --git a/scripts/hooks/hook-pnoise.py b/scripts/hooks/hook-pnoise.py
new file mode 100644
--- /dev/null
+++ b/scripts/hooks/hook-pnoise.py
@@ -0,0 +1,3 @@
+from PyInstaller.utils.hooks import collect_all
+
+datas, binaries, hiddenimports = collect_all("pnoise")


Test patch:
Repository: abey79/vpype

Issue:
Remove decimal point from VS command
In some edge cases VS1.0 is not read by HP plotters. A VS1 command is better. 
Remove decimal point from VS command
In some edge cases VS1.0 is not read by HP plotters. A VS1 command is better. 


Discussion:

Resolving patch:
diff --git a/vpype/io.py b/vpype/io.py
--- a/vpype/io.py
+++ b/vpype/io.py
@@ -494,7 +494,7 @@ def write_hpgl(
     landscape: bool,
     center: bool,
     device: Optional[str],
-    velocity: Optional[float],
+    velocity: Optional[int],
     quiet: bool = False,
 ) -> None:
     """Create a HPGL file from the :class:`Document` instance.
@@ -608,7 +608,7 @@ def complex_to_str(p: complex) -> str:
 
     output.write("IN;DF;")
     if velocity is not None:
-        output.write(f"VS{velocity};")
+        output.write(f"VS{int(velocity)};")
     if paper_config.set_ps is not None:
         output.write(f"PS{int(paper_config.set_ps)};")
 
diff --git a/vpype_cli/write.py b/vpype_cli/write.py
--- a/vpype_cli/write.py
+++ b/vpype_cli/write.py
@@ -152,7 +152,7 @@
 @click.option(
     "-vs",
     "--velocity",
-    type=float,
+    type=int,
     help="[HPGL only] Emit a VS command with the provided value.",
 )
 @click.option(


Test patch:
Repository: abey79/vpype

Issue:
Dependency: remplace `toml` by `tomli`
Compared to `toml`, `tomli` is faster, lighter (read-only), more compliant, and better maintained. Its likely the better option in the long run.


Discussion:

Resolving patch:
diff --git a/vpype/config.py b/vpype/config.py
--- a/vpype/config.py
+++ b/vpype/config.py
@@ -14,14 +14,14 @@
     PaperConfig(name='a4', paper_size=(1122.5196850393702, 793.7007874015749), ...)
 """
 
+import dataclasses
 import logging
 import math
 import os
 import pathlib
 from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
 
-import attr
-import toml
+import tomli
 
 from .utils import convert_length
 
@@ -38,7 +38,7 @@ def _convert_length_pair(data: Sequence[Union[float, str]]) -> Tuple[float, floa
     return convert_length(data[0]), convert_length(data[1])
 
 
-@attr.s(auto_attribs=True, frozen=True)
+@dataclasses.dataclass(frozen=True)
 class PaperConfig:
     """Data class containing configuration for a give plotter type/paper size combinations."""
 
@@ -62,9 +62,9 @@ class PaperConfig:
     final_pu_params: Optional[
         str
     ] = None  #: if not None, these params are added to the final PU command
-    aka_names: List[
-        str
-    ] = []  #: alternative paper names (will be found by :func:`paper_config`
+    aka_names: List[str] = dataclasses.field(
+        default_factory=list
+    )  #: alternative paper names (will be found by :func:`paper_config`
 
     @classmethod
     def from_config(cls, data: Dict[str, Any]) -> "PaperConfig":
@@ -87,7 +87,7 @@ def from_config(cls, data: Dict[str, Any]) -> "PaperConfig":
         )
 
 
-@attr.s(auto_attribs=True, frozen=True)
+@dataclasses.dataclass(frozen=True)
 class PlotterConfig:
     """Data class containing configuration for a given plotter type."""
 
@@ -193,7 +193,8 @@ def _update(d: Dict, u: Mapping) -> Dict:
             return d
 
         logging.info(f"loading config file at {path}")
-        self._config = _update(self._config, toml.load(path))
+        with open(path, "rb") as fp:
+            self._config = _update(self._config, tomli.load(fp))
 
     def get_plotter_list(self) -> List[str]:
         """Returns a list of plotter names whose configuration is available.
diff --git a/vpype_cli/cli.py b/vpype_cli/cli.py
--- a/vpype_cli/cli.py
+++ b/vpype_cli/cli.py
@@ -13,7 +13,8 @@
 
 import vpype as vp
 
-__all__ = ("cli", "execute", "begin", "end")
+# Note: BlockProcessor added to the list for the purpose of building the documentation
+__all__ = ("BlockProcessor", "cli", "execute", "begin", "end")
 
 
 class GroupedGroup(click.Group):
diff --git a/vpype_cli/layerops.py b/vpype_cli/layerops.py
--- a/vpype_cli/layerops.py
+++ b/vpype_cli/layerops.py
@@ -7,7 +7,7 @@
 
 from .cli import cli
 
-__all__ = ("lcopy", "lmove", "ldelete", "lswap")
+__all__ = ("lcopy", "lmove", "ldelete", "lreverse", "lswap")
 
 
 @cli.command(group="Layers")


Test patch:
Repository: abey79/vpype

Issue:
Feature: "page rotate" command
Equivalent to `rotate`+`translate` and changing pagesize orientation, eg.:

```
vpype [...] rotate -o 0 0 -- -90 translate 0cm 21cm pagesize -l a4
```

cc @Hapiel


Discussion:
This would be great to have :)

Resolving patch:
diff --git a/vpype_cli/operations.py b/vpype_cli/operations.py
--- a/vpype_cli/operations.py
+++ b/vpype_cli/operations.py
@@ -1,4 +1,5 @@
 import logging
+import math
 from typing import List, Optional, Tuple, Union, cast
 
 import click
@@ -18,6 +19,7 @@
     "linesimplify",
     "linesort",
     "multipass",
+    "pagerotate",
     "pagesize",
     "reloop",
     "reverse",
@@ -609,6 +611,39 @@ def layout(
     return document
 
 
+@cli.command(group="Operations")
+@click.option(
+    "--clockwise",
+    "-cw",
+    is_flag=True,
+    help="Rotate clockwise instead of the default counter-clockwise",
+)
+@global_processor
+def pagerotate(document: vp.Document, clockwise: bool):
+    """Rotate the page by 90 degrees.
+
+    This command rotates the page by 90 degrees counter-clockwise. If the `--clockwise` option
+    is passed, it rotates the page clockwise instead.
+
+    Note: if the page size is not defined, an error is printed and the page is not rotated.
+    """
+    page_size = document.page_size
+    if page_size is None:
+        logging.warning("pagerotate: page size is not defined, page not rotated")
+        return document
+    w, h = page_size
+
+    if clockwise:
+        document.rotate(math.pi / 2)
+        document.translate(h, 0)
+    else:
+        document.rotate(-math.pi / 2)
+        document.translate(0, w)
+
+    document.page_size = h, w
+    return document
+
+
 @cli.command(group="Operations")
 @click.argument("pitch", type=LengthType(), required=True)
 @layer_processor


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -98,6 +98,7 @@ class Command:
     ),
     Command("eval x=2 eval %y=3 eval z=4% eval %w=5%"),
     Command("forlayer text '%_lid% (%_i%/%_n%): %_name%' end"),
+    Command("pagerotate", keeps_page_size=False),
 ]
 
 # noinspection SpellCheckingInspection
@@ -687,3 +688,17 @@ def test_forlayer_command_property_accessor():
     for i in range(3):
         assert doc.layers[i + 1].property("test") == i
         assert doc.layers[i + 1].property("test2") == i
+
+
+def test_pagerotate():
+    doc = vpype_cli.execute("random pagesize a4 pagerotate")
+    assert doc.page_size == pytest.approx((1122.5196850393702, 793.7007874015749))
+
+    doc = vpype_cli.execute("random pagesize a4 pagerotate -cw")
+    assert doc.page_size == pytest.approx((1122.5196850393702, 793.7007874015749))
+
+
+def test_pagerotate_error(caplog):
+    doc = vpype_cli.execute("random pagerotate")
+    assert doc.page_size is None
+    assert "page size is not defined, page not rotated" in caplog.text