From 4a498d7922456c277ab2240145241fa9ffc4c263 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Thu, 8 May 2025 17:38:08 +0200 Subject: [PATCH 01/14] add missing return type hints --- drawBot/drawBotDrawingTools.py | 164 +++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 885478cb..2476f578 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -8,7 +8,7 @@ import AppKit # type: ignore import CoreText # type: ignore import Quartz # type: ignore - +from .drawBotPageDrawingTools import DrawBotPage from .aliases import ( BoundingBox, CMYKColor, @@ -160,7 +160,7 @@ def _copy(self): new._tempInstalledFonts = dict(self._tempInstalledFonts) return new - def newDrawing(self): + def newDrawing(self) -> None: """ Reset the drawing stack to the clean and empty stack. @@ -181,7 +181,7 @@ def newDrawing(self): """ self._reset() - def endDrawing(self): + def endDrawing(self) -> None: """ Explicitly tell drawBot the drawing is done. This is advised when using drawBot as a standalone module. @@ -257,7 +257,7 @@ def pageCount(self) -> int: # size and pages - def size(self, width: float | str, height: float | None = None): + def size(self, width: float | str, height: float | None = None) -> None: """ Set the width and height of the canvas. Without calling `size()` the default drawing board is 1000 by 1000 points. @@ -299,7 +299,7 @@ def size(self, width: float | str, height: float | None = None): else: raise DrawBotError("Can't use 'size()' after drawing has begun. Try to move it to the top of your script.") - def newPage(self, width: str | float | None = None, height: float | None = None): + def newPage(self, width: str | float | None = None, height: float | None = None) -> None: """ Create a new canvas to draw in. This will act like a page in a pdf or a frame in a mov. @@ -338,7 +338,7 @@ def newPage(self, width: str | float | None = None, height: float | None = None) self._dummyContext = DummyContext() self._addInstruction("newPage", width, height) - def pages(self): + def pages(self) -> tuple[DrawBotPage, ...]: """ Return all pages. @@ -380,8 +380,6 @@ def pages(self): # draw an oval in each of them oval(110, 10, 30, 30) """ - from .drawBotPageDrawingTools import DrawBotPage - instructions = [] for instructionSet in self._instructionsStack: for callback, _, _ in instructionSet: @@ -476,7 +474,7 @@ def saveImage(self, path: SomePath, *args, **options: dict[str, Any]): supportedOptions="\n ".join(getContextOptionsDocs()), ) - def printImage(self, pdf=None): + def printImage(self, pdf=None) -> None: """ Export the canvas to a printing dialog, ready to print. @@ -510,7 +508,7 @@ def pdfImage(self): # graphics state - def save(self): + def save(self) -> None: """ DrawBot strongly recommends to use `savedState()` in a `with` statement instead. @@ -522,7 +520,7 @@ def save(self): self._requiresNewFirstPage = True self._addInstruction("save") - def restore(self): + def restore(self) -> None: """ DrawBot strongly recommends to use `savedState()` in a `with` statement instead. @@ -565,7 +563,7 @@ def savedState(self): # basic shapes - def rect(self, x: float, y: float, w: float, h: float): + def rect(self, x: float, y: float, w: float, h: float) -> None: """ Draw a rectangle from position x, y with the given width and height. @@ -578,7 +576,7 @@ def rect(self, x: float, y: float, w: float, h: float): self._requiresNewFirstPage = True self._addInstruction("rect", x, y, w, h) - def oval(self, x: float, y: float, w: float, h: float): + def oval(self, x: float, y: float, w: float, h: float) -> None: """ Draw an oval from position x, y with the given width and height. @@ -593,14 +591,14 @@ def oval(self, x: float, y: float, w: float, h: float): # path - def newPath(self): + def newPath(self) -> None: """ Create a new path. """ self._requiresNewFirstPage = True self._addInstruction("newPath") - def moveTo(self, xy: Point): + def moveTo(self, xy: Point) -> None: """ Move to a point `x`, `y`. """ @@ -608,7 +606,7 @@ def moveTo(self, xy: Point): self._requiresNewFirstPage = True self._addInstruction("moveTo", (x, y)) - def lineTo(self, xy: Point): + def lineTo(self, xy: Point) -> None: """ Line to a point `x`, `y`. """ @@ -616,7 +614,7 @@ def lineTo(self, xy: Point): self._requiresNewFirstPage = True self._addInstruction("lineTo", (x, y)) - def curveTo(self, xy1: Point, xy2: Point, xy3: Point): + def curveTo(self, xy1: Point, xy2: Point, xy3: Point) -> None: """ Curve to a point `x3`, `y3`. With given bezier handles `x1`, `y1` and `x2`, `y2`. @@ -627,7 +625,7 @@ def curveTo(self, xy1: Point, xy2: Point, xy3: Point): self._requiresNewFirstPage = True self._addInstruction("curveTo", (x1, y1), (x2, y2), (x3, y3)) - def qCurveTo(self, *points: Point): + def qCurveTo(self, *points: Point) -> None: """ Quadratic curve with a given set of off curves to a on curve. """ @@ -641,14 +639,14 @@ def arc( startAngle: float, endAngle: float, clockwise: bool, - ): + ) -> None: """ Arc with `center` and a given `radius`, from `startAngle` to `endAngle`, going clockwise if `clockwise` is True and counter clockwise if `clockwise` is False. """ self._requiresNewFirstPage = True self._addInstruction("arc", center, radius, startAngle, endAngle, clockwise) - def arcTo(self, xy1: Point, xy2: Point, radius: float): + def arcTo(self, xy1: Point, xy2: Point, radius: float) -> None: """ Arc from one point to an other point with a given `radius`. @@ -688,14 +686,14 @@ def drawPt(pos, r=5): self._requiresNewFirstPage = True self._addInstruction("arcTo", (x1, y1), (x2, y2), radius) - def closePath(self): + def closePath(self) -> None: """ Close the path. """ self._requiresNewFirstPage = True self._addInstruction("closePath") - def drawPath(self, path: BezierPath | None = None): + def drawPath(self, path: BezierPath | None = None) -> None: """ Draw the current path, or draw the provided path. @@ -724,7 +722,7 @@ def drawPath(self, path: BezierPath | None = None): self._requiresNewFirstPage = True self._addInstruction("drawPath", path) - def clipPath(self, path=None): + def clipPath(self, path=None) -> None: """ Use the given path as a clipping path, or the current path if no path was given. @@ -756,7 +754,7 @@ def clipPath(self, path=None): self._requiresNewFirstPage = True self._addInstruction("clipPath", path) - def line(self, point1: Point, point2: Point): + def line(self, point1: Point, point2: Point) -> None: """ Draws a line between two given points. @@ -771,7 +769,7 @@ def line(self, point1: Point, point2: Point): path.line(point1, point2) self.drawPath(path) - def polygon(self, *points: Point, **kwargs: bool): + def polygon(self, *points: Point, **kwargs: bool) -> None: """ Draws a polygon with n-amount of points. Optionally a `close` argument can be provided to open or close the path. @@ -788,7 +786,7 @@ def polygon(self, *points: Point, **kwargs: bool): # color - def colorSpace(self, colorSpace): + def colorSpace(self, colorSpace) -> None: """ Set the color space. Options are `genericRGB`, `adobeRGB1998`, `sRGB`, `genericGray`, `genericGamma22Gray`. @@ -826,7 +824,7 @@ def listColorSpaces(self) -> list[str]: """ return sorted(self._dummyContext._colorSpaceMap.keys()) - def blendMode(self, operation: str): + def blendMode(self, operation: str) -> None: """ Set a blend mode. @@ -862,7 +860,7 @@ def fill( g: float | None = None, b: float | None = None, alpha: float = 1, - ): + ) -> None: """ Sets the fill color with a `red`, `green`, `blue` and `alpha` value. Each argument must a value float between 0 and 1. @@ -902,7 +900,7 @@ def stroke( g: float | None = None, b: float | None = None, alpha: float = 1, - ): + ) -> None: """ Sets the stroke color with a `red`, `green`, `blue` and `alpha` value. Each argument must a value float between 0 and 1. @@ -947,7 +945,7 @@ def cmykFill( y: float | None = None, k: float | None = None, alpha: float = 1, - ): + ) -> None: """ Set a fill using a CMYK color before drawing a shape. This is handy if the file is intended for print. @@ -978,7 +976,7 @@ def cmykStroke( y: float | None = None, k: float | None = None, alpha: float = 1, - ): + ) -> None: """ Set a stroke using a CMYK color before drawing a shape. This is handy if the file is intended for print. @@ -1006,7 +1004,7 @@ def cmykStroke( self._requiresNewFirstPage = True self._addInstruction("cmykStroke", c, m, y, k, alpha) - def opacity(self, value: float): + def opacity(self, value: float) -> None: """ Sets the current opacity value. The `value` argument must be a value between 0.0 and 1.0. @@ -1031,7 +1029,7 @@ def shadow( offset: Point, blur: float | None = None, color: tuple[float, ...] | None = None, - ): + ) -> None: """ Adds a shadow with an `offset` (x, y), `blur` and a `color`. The `color` argument must be a tuple similarly as `fill`. @@ -1056,7 +1054,7 @@ def cmykShadow( offset: Point, blur: float | None = None, color: tuple[float, ...] | None = None, - ): + ) -> None: """ Adds a cmyk shadow with an `offset` (x, y), `blur` and a `color`. The `color` argument must be a tuple similarly as `cmykFill`. @@ -1113,7 +1111,7 @@ def cmykLinearGradient( endPoint: Point | None = None, colors: list[CMYKColorTuple] | None = None, locations=None, - ): + ) -> None: """ A cmyk linear gradient fill with: @@ -1147,7 +1145,7 @@ def radialGradient( locations: list[float] | None = None, startRadius: float = 0, endRadius: float = 100, - ): + ) -> None: """ A radial gradient fill with: @@ -1185,7 +1183,7 @@ def cmykRadialGradient( locations: list[float] | None = None, startRadius: float = 0, endRadius: float = 100, - ): + ) -> None: """ A cmyk radial gradient fill with: @@ -1217,7 +1215,7 @@ def cmykRadialGradient( # path drawing behavoir - def strokeWidth(self, value: float): + def strokeWidth(self, value: float) -> None: """ Sets stroke width. @@ -1239,7 +1237,7 @@ def strokeWidth(self, value: float): self._requiresNewFirstPage = True self._addInstruction("strokeWidth", value) - def miterLimit(self, value: float): + def miterLimit(self, value: float) -> None: """ Set a miter limit. Used on corner points. @@ -1393,7 +1391,7 @@ def lineDash(self, value: float | None, *values: float, offset: float = 0): # transform - def transform(self, matrix: TransformTuple, center: Point = (0, 0)): + def transform(self, matrix: TransformTuple, center: Point = (0, 0)) -> None: """ Transform the canvas with a transformation matrix. """ @@ -1402,13 +1400,13 @@ def transform(self, matrix: TransformTuple, center: Point = (0, 0)): matrix = transformationAtCenter(matrix, center) self._addInstruction("transform", matrix) - def translate(self, x: float = 0, y: float = 0): + def translate(self, x: float = 0, y: float = 0) -> None: """ Translate the canvas with a given offset. """ self.transform((1, 0, 0, 1, x, y)) - def rotate(self, angle: float, center: Point = (0, 0)): + def rotate(self, angle: float, center: Point = (0, 0)) -> None: """ Rotate the canvas around the `center` point (which is the origin by default) with a given angle in degrees. """ @@ -1417,7 +1415,7 @@ def rotate(self, angle: float, center: Point = (0, 0)): s = math.sin(angle) self.transform((c, s, -s, c, 0, 0), center) - def scale(self, x: float = 1, y: float | None = None, center: Point = (0, 0)): + def scale(self, x: float = 1, y: float | None = None, center: Point = (0, 0)) -> None: """ Scale the canvas with a given `x` (horizontal scale) and `y` (vertical scale). @@ -1429,7 +1427,7 @@ def scale(self, x: float = 1, y: float | None = None, center: Point = (0, 0)): y = x self.transform((x, 0, 0, y, 0, 0), center) - def skew(self, angle1: float, angle2: float = 0, center: Point = (0, 0)): + def skew(self, angle1: float, angle2: float = 0, center: Point = (0, 0)) -> None: """ Skew the canvas with given `angle1` and `angle2`. @@ -1443,7 +1441,12 @@ def skew(self, angle1: float, angle2: float = 0, center: Point = (0, 0)): # text - def font(self, fontNameOrPath: SomePath, fontSize: float | None = None, fontNumber: int = 0): + def font( + self, + fontNameOrPath: SomePath, + fontSize: float | None = None, + fontNumber: int = 0, + ) -> str | None: """ Set a font with the name of the font. If a font path is given the font will be installed and used directly. @@ -1465,7 +1468,7 @@ def font(self, fontNameOrPath: SomePath, fontSize: float | None = None, fontNumb self._addInstruction("font", fontNameOrPath, fontSize, fontNumber) return getFontName(font) - def fallbackFont(self, fontNameOrPath: SomePath, fontNumber: int = 0): + def fallbackFont(self, fontNameOrPath: SomePath, fontNumber: int = 0) -> str | None: """ Set a fallback font, this is used whenever a glyph is not available in the current font. @@ -1480,7 +1483,7 @@ def fallbackFont(self, fontNameOrPath: SomePath, fontNumber: int = 0): self._addInstruction("fallbackFont", fontNameOrPath, fontNumber) return getFontName(dummyFont) - def fontSize(self, fontSize: float): + def fontSize(self, fontSize: float) -> None: """ Set the font size in points. The default `fontSize` is 10pt. @@ -1508,7 +1511,7 @@ def lineHeight(self, value): self._dummyContext.lineHeight(value) self._addInstruction("lineHeight", value) - def tracking(self, value: float): + def tracking(self, value: float) -> None: """ Set the tracking between characters. It adds an absolute number of points between the characters. @@ -1530,14 +1533,14 @@ def tracking(self, value: float): self._dummyContext.tracking(value) self._addInstruction("tracking", value) - def baselineShift(self, value): + def baselineShift(self, value) -> None: """ Set the shift of the baseline. """ self._dummyContext.baselineShift(value) self._addInstruction("baselineShift", value) - def underline(self, value: str): + def underline(self, value: str | None) -> None: # FIXME could we assert the value in entrance? """ Set the underline value. Underline must be `single`, `thick`, `double` or `None`. @@ -1551,7 +1554,7 @@ def underline(self, value: str): self._dummyContext.underline(value) self._addInstruction("underline", value) - def strikethrough(self, value: str): + def strikethrough(self, value: str | None) -> None: # FIXME could we assert the value in entrance? """ Set the strikethrough value. Underline must be `single`, `thick`, `double` or `None`. @@ -1566,7 +1569,7 @@ def strikethrough(self, value: str): self._dummyContext.strikethrough(value) self._addInstruction("strikethrough", value) - def url(self, value: str): + def url(self, value: str) -> None: """ Set the url value for text. @@ -1579,7 +1582,7 @@ def url(self, value: str): self._dummyContext.url(value) self._addInstruction("url", value) - def hyphenation(self, value: bool): + def hyphenation(self, value: bool) -> None: """ Set hyphenation, `True` or `False`. @@ -1597,7 +1600,7 @@ def hyphenation(self, value: bool): self._checkLanguageHyphenation() self._addInstruction("hyphenation", value) - def tabs(self, *tabs: tuple[float, str]): + def tabs(self, *tabs: tuple[float, str]) -> None: r""" Set tabs, tuples of (`float`, `alignment`) Aligment can be `"left"`, `"center"`, `"right"` or any other character. @@ -1624,7 +1627,7 @@ def tabs(self, *tabs: tuple[float, str]): self._dummyContext.tabs(*tabs) self._addInstruction("tabs", *tabs) - def language(self, language): + def language(self, language) -> None: """ Set the preferred language as language tag or None to use the default language. A language tag might be a [iso639-2 or iso639-1](https://www.loc.gov/standards/iso639-2/php/English_list.php) @@ -1674,7 +1677,9 @@ def _checkLanguageHyphenation(self): if not CoreText.CFStringIsHyphenationAvailableForLocale(locale): warnings.warn(f"Language '{language}' has no hyphenation available.") - def writingDirection(self, direction: str | None): + def writingDirection( + self, direction: str | None + ) -> None: # FIXME could we assert the value in entrance? or give a warning? """ Set the writing direction: `None`, `'LTR'` or `'RTL'`. @@ -1734,7 +1739,7 @@ def listOpenTypeFeatures(self, fontNameOrPath: SomePath | None = None) -> list[s listOpenTypeFeatures.__doc__ = FormattedString.listOpenTypeFeatures.__doc__ - def fontVariations(self, *args: None, **axes: float | bool): + def fontVariations(self, *args: None, **axes: float) -> dict[str, float]: # FIXME why was bool there? also, why *args? """ Pick a variation by axes values. @@ -1766,7 +1771,7 @@ def listFontVariations(self, fontNameOrPath: SomePath | None = None) -> dict[str listFontVariations.__doc__ = FormattedString.listFontVariations.__doc__ - def fontNamedInstance(self, name: str, fontNameOrPath: SomePath | None = None): + def fontNamedInstance(self, name: str, fontNameOrPath: SomePath | None = None) -> None: """ Set a font with `name` of a named instance. The `name` of the named instance must be listed in `listNamedInstances()`, @@ -1798,7 +1803,7 @@ def textProperties(self) -> dict[str, Any]: # drawing text - def text(self, txt: FormattedString | str, position: Point, align: str | None = None): + def text(self, txt: FormattedString | str, position: Point, align: str | None = None) -> None: """ Draw a text at a provided position. @@ -1830,7 +1835,12 @@ def text(self, txt: FormattedString | str, position: Point, align: str | None = subTxt.copyContextProperties(txt) self.textBox(subTxt, box, align=align) - def textOverflow(self, txt: FormattedString | str, box: BoundingBox, align: str | None = None): + def textOverflow( + self, + txt: FormattedString | str, + box: BoundingBox | BezierPath, + align: str | None = None, + ) -> FormattedString | str | None: """ Returns the overflowed text without drawing the text. @@ -1854,7 +1864,9 @@ def textOverflow(self, txt: FormattedString | str, box: BoundingBox, align: str raise DrawBotError("align must be %s" % (", ".join(self._dummyContext._textAlignMap.keys()))) return self._dummyContext.clippedText(txt, box, align) - def textBox(self, txt: FormattedString | str, box: BoundingBox, align: str | None = None): + def textBox( + self, txt: FormattedString | str, box: BoundingBox, align: str | None = None + ) -> str | FormattedString: # FIXME is this correct? """ Draw a text in a provided rectangle. @@ -1992,7 +2004,9 @@ def textBox(self, txt: FormattedString | str, box: BoundingBox, align: str | Non self._addInstruction("textBox", txt, box, align) return self._dummyContext.clippedText(txt, box, align) - def textBoxBaselines(self, txt: FormattedString | str, box: BoundingBox, align: str | None = None): + def textBoxBaselines( + self, txt: FormattedString | str, box: BoundingBox, align: str | None = None + ) -> list[tuple[float, float]]: """ Returns a list of `x, y` coordinates indicating the start of each line @@ -2013,7 +2027,10 @@ def textBoxBaselines(self, txt: FormattedString | str, box: BoundingBox, align: origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) return [(x + o.x, y + o.y) for o in origins] - def textBoxCharacterBounds(self, txt: FormattedString | str, box: BoundingBox, align: str | None = None): + # FIXME align is not used + def textBoxCharacterBounds( + self, txt: FormattedString | str, box: BoundingBox, align: str | None = None + ) -> list[tuple[BoundingBox | BezierPath, float, FormattedString | str]]: """ Returns a list of typesetted bounding boxes `((x, y, w, h), baseLineOffset, formattedSubString)`. @@ -2057,7 +2074,7 @@ def image( position: Point, alpha: float = 1, pageNumber: int | None = None, - ): + ) -> None: """ Add an image from a `path` with an `offset` and an `alpha` value. This accepts most common file types like pdf, jpg, png, tiff and gif. `NSImage` objects are accepted too. @@ -2203,9 +2220,16 @@ def imagePixelColor( if color is None: return None color = color.colorUsingColorSpaceName_("NSCalibratedRGBColorSpace") - return color.redComponent(), color.greenComponent(), color.blueComponent(), color.alphaComponent() + return ( + color.redComponent(), + color.greenComponent(), + color.blueComponent(), + color.alphaComponent(), + ) - def imageResolution(self, path: SomePath | AppKit.NSImage) -> int: + def imageResolution( + self, path: SomePath | AppKit.NSImage + ) -> int: # FIXME how can it be an integer with that kind of arithmetic operation? """ Return the image resolution for a given image. Supports pdf, jpg, png, tiff and gif file formats. `NSImage` objects are supported too. """ @@ -2257,7 +2281,7 @@ def numberOfPages(self, path: SomePath) -> int | None: # mov - def frameDuration(self, seconds: float): + def frameDuration(self, seconds: float) -> None: """ When exporting to `mov` or `gif` each frame can have duration set in `seconds`. @@ -2378,7 +2402,7 @@ def textSize( align: str | None = None, width: float | None = None, height: float | None = None, - ): + ) -> tuple[float, float]: """ Returns the size of a text with the current settings, like `font`, `fontSize` and `lineHeight` as a tuple (width, height). @@ -2458,7 +2482,7 @@ def installFont(self, path: SomePath) -> str: warnings.warn("install font: %s" % error) return psName - def uninstallFont(self, path: SomePath): + def uninstallFont(self, path: SomePath) -> None: """ Uninstall a font with a given path. @@ -2549,7 +2573,7 @@ def fontLineHeight(self) -> float: """ return self._dummyContext._state.text.fontLineHeight() - def Variable(self, variables, workSpace, continuous=True): + def Variable(self, variables, workSpace, continuous=True) -> None: """ Build small UI for variables in a script. From 0f522e03ff58e47c3b123e851b3eb3d18c02f727 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Thu, 8 May 2025 17:38:29 +0200 Subject: [PATCH 02/14] fix annotations (with note) --- drawBot/drawBotDrawingTools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 2476f578..af427475 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -388,7 +388,7 @@ def pages(self) -> tuple[DrawBotPage, ...]: break return tuple(DrawBotPage(instructionSet) for instructionSet in instructions) - def saveImage(self, path: SomePath, *args, **options: dict[str, Any]): + def saveImage(self, path: SomePath, *args: Any, **options: Any): # FIXME how could we annotate this one? """ Save or export the canvas to a specified format. The `path` argument is a single destination path to save the current drawing actions. From 8635b994c9fe08930b1777d0e7e2c7db287d2ae7 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Thu, 8 May 2025 17:38:39 +0200 Subject: [PATCH 03/14] notes --- drawBot/drawBotDrawingTools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index af427475..56ab1741 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -496,7 +496,7 @@ def printImage(self, pdf=None) -> None: else: context.printImage(pdf) - def pdfImage(self): + def pdfImage(self): # FIXME how could we annotate this? """ Return the image as a pdf document object. """ @@ -1698,7 +1698,7 @@ def writingDirection( self._dummyContext.writingDirection(direction) self._addInstruction("writingDirection", direction) - def openTypeFeatures(self, *args: bool | None, **features: bool) -> dict[str, bool]: + def openTypeFeatures(self, *args: bool | None, **features: bool) -> dict[str, bool]: # FIXME I am sure we discussed this already, but why do we need args here? From the example is not evident... """ Enable OpenType features. From 59f950e111c98901d316a4c3a838a571885fbd3a Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Thu, 8 May 2025 17:40:16 +0200 Subject: [PATCH 04/14] ruff --- drawBot/drawBotDrawingTools.py | 42 +++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 56ab1741..2e17f939 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -1173,7 +1173,15 @@ def radialGradient( rect(10, 10, 980, 980) """ self._requiresNewFirstPage = True - self._addInstruction("radialGradient", startPoint, endPoint, colors, locations, startRadius, endRadius) + self._addInstruction( + "radialGradient", + startPoint, + endPoint, + colors, + locations, + startRadius, + endRadius, + ) def cmykRadialGradient( self, @@ -1211,7 +1219,15 @@ def cmykRadialGradient( rect(10, 10, 980, 980) """ self._requiresNewFirstPage = True - self._addInstruction("cmykRadialGradient", startPoint, endPoint, colors, locations, startRadius, endRadius) + self._addInstruction( + "cmykRadialGradient", + startPoint, + endPoint, + colors, + locations, + startRadius, + endRadius, + ) # path drawing behavoir @@ -1698,7 +1714,11 @@ def writingDirection( self._dummyContext.writingDirection(direction) self._addInstruction("writingDirection", direction) - def openTypeFeatures(self, *args: bool | None, **features: bool) -> dict[str, bool]: # FIXME I am sure we discussed this already, but why do we need args here? From the example is not evident... + def openTypeFeatures( + self, *args: bool | None, **features: bool + ) -> dict[ + str, bool + ]: # FIXME I am sure we discussed this already, but why do we need args here? From the example is not evident... """ Enable OpenType features. @@ -1739,7 +1759,9 @@ def listOpenTypeFeatures(self, fontNameOrPath: SomePath | None = None) -> list[s listOpenTypeFeatures.__doc__ = FormattedString.listOpenTypeFeatures.__doc__ - def fontVariations(self, *args: None, **axes: float) -> dict[str, float]: # FIXME why was bool there? also, why *args? + def fontVariations( + self, *args: None, **axes: float + ) -> dict[str, float]: # FIXME why was bool there? also, why *args? """ Pick a variation by axes values. @@ -1829,7 +1851,10 @@ def text(self, txt: FormattedString | str, position: Point, align: str | None = raise DrawBotError("align must be left, right, center") attributedString = self._dummyContext.attributedString(txt, align=align) for subTxt, box in makeTextBoxes( - attributedString, (x, y), align=align, plainText=not isinstance(txt, FormattedString) + attributedString, + (x, y), + align=align, + plainText=not isinstance(txt, FormattedString), ): if isinstance(txt, FormattedString): subTxt.copyContextProperties(txt) @@ -2060,7 +2085,12 @@ def textBoxCharacterBounds( runW, runH, ascent, descent = CoreText.CTRunGetTypographicBounds(ctRun, (0, 0), None, None, None) bounds.append( CharactersBounds( - (x + originX + runPos.x, y + originY + runPos.y - ascent, runW, runH + ascent), + ( + x + originX + runPos.x, + y + originY + runPos.y - ascent, + runW, + runH + ascent, + ), ascent, txt[runRange.location : runRange.location + runRange.length], ) From fd1c1fec92a14f643d3e4edd8819127d83ace829 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Thu, 8 May 2025 17:55:01 +0200 Subject: [PATCH 05/14] improve hint --- drawBot/drawBotDrawingTools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 2e17f939..89282918 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -1890,8 +1890,8 @@ def textOverflow( return self._dummyContext.clippedText(txt, box, align) def textBox( - self, txt: FormattedString | str, box: BoundingBox, align: str | None = None - ) -> str | FormattedString: # FIXME is this correct? + self, txt: FormattedString | str, box: BoundingBox, align: str | None = None, + ) -> str | FormattedString | None: """ Draw a text in a provided rectangle. From ce535a65331b145c6185f7892b17de3d0dcde403 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Thu, 8 May 2025 17:57:43 +0200 Subject: [PATCH 06/14] avoid issue with circular import --- drawBot/drawBotDrawingTools.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 89282918..d7cdd7e7 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -8,7 +8,6 @@ import AppKit # type: ignore import CoreText # type: ignore import Quartz # type: ignore -from .drawBotPageDrawingTools import DrawBotPage from .aliases import ( BoundingBox, CMYKColor, @@ -29,6 +28,7 @@ getNSFontFromNameOrPath, makeTextBoxes, ) +from typing import TYPE_CHECKING from .context.dummyContext import DummyContext from .context.tools import drawBotbuiltins, gifTools from .context.tools.imageObject import ImageObject @@ -44,6 +44,8 @@ warnings, ) +if TYPE_CHECKING: + from .drawBotPageDrawingTools import DrawBotPage def _getmodulecontents(module, names=None): d = {} @@ -338,7 +340,8 @@ def newPage(self, width: str | float | None = None, height: float | None = None) self._dummyContext = DummyContext() self._addInstruction("newPage", width, height) - def pages(self) -> tuple[DrawBotPage, ...]: + + def pages(self) -> tuple["DrawBotPage", ...]: """ Return all pages. @@ -380,6 +383,8 @@ def pages(self) -> tuple[DrawBotPage, ...]: # draw an oval in each of them oval(110, 10, 30, 30) """ + from .drawBotPageDrawingTools import DrawBotPage + instructions = [] for instructionSet in self._instructionsStack: for callback, _, _ in instructionSet: From 4f5b82600e1d59a48f3544065bb4c013684355b6 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Fri, 9 May 2025 09:47:01 +0200 Subject: [PATCH 07/14] defer evalutation of the annotations --- drawBot/drawBotDrawingTools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index d7cdd7e7..f1f71130 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import os import random @@ -340,8 +342,7 @@ def newPage(self, width: str | float | None = None, height: float | None = None) self._dummyContext = DummyContext() self._addInstruction("newPage", width, height) - - def pages(self) -> tuple["DrawBotPage", ...]: + def pages(self) -> tuple[DrawBotPage, ...]: """ Return all pages. From 6cf18caa45224a46e5ad0567470866a3e6feed04 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Fri, 9 May 2025 09:47:09 +0200 Subject: [PATCH 08/14] sort imports --- drawBot/drawBotDrawingTools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index f1f71130..f012b2e7 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -5,11 +5,12 @@ import random from collections import namedtuple from contextlib import contextmanager -from typing import Any +from typing import TYPE_CHECKING, Any import AppKit # type: ignore import CoreText # type: ignore import Quartz # type: ignore + from .aliases import ( BoundingBox, CMYKColor, @@ -30,7 +31,6 @@ getNSFontFromNameOrPath, makeTextBoxes, ) -from typing import TYPE_CHECKING from .context.dummyContext import DummyContext from .context.tools import drawBotbuiltins, gifTools from .context.tools.imageObject import ImageObject From fd607c73934cf85a84ee5a822e975f124ba7ed70 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Fri, 9 May 2025 09:47:25 +0200 Subject: [PATCH 09/14] improve hints --- drawBot/drawBotDrawingTools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index f012b2e7..bbab4d8c 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -394,7 +394,9 @@ def pages(self) -> tuple[DrawBotPage, ...]: break return tuple(DrawBotPage(instructionSet) for instructionSet in instructions) - def saveImage(self, path: SomePath, *args: Any, **options: Any): # FIXME how could we annotate this one? + def saveImage( + self, path: SomePath, *args: Any, **options: Any + ) -> list[AppKit.NSImage | PIL.ImageFile.ImageFile] | None: """ Save or export the canvas to a specified format. The `path` argument is a single destination path to save the current drawing actions. @@ -502,7 +504,7 @@ def printImage(self, pdf=None) -> None: else: context.printImage(pdf) - def pdfImage(self): # FIXME how could we annotate this? + def pdfImage(self) -> Quartz.PDFDocument: """ Return the image as a pdf document object. """ @@ -2263,9 +2265,7 @@ def imagePixelColor( color.alphaComponent(), ) - def imageResolution( - self, path: SomePath | AppKit.NSImage - ) -> int: # FIXME how can it be an integer with that kind of arithmetic operation? + def imageResolution(self, path: SomePath | AppKit.NSImage) -> float: """ Return the image resolution for a given image. Supports pdf, jpg, png, tiff and gif file formats. `NSImage` objects are supported too. """ From 74cf1bcaf4d74e62c018b3b458324a3bdfbcfa39 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Fri, 9 May 2025 09:47:29 +0200 Subject: [PATCH 10/14] format --- drawBot/drawBotDrawingTools.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index bbab4d8c..37f0191b 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -49,6 +49,7 @@ if TYPE_CHECKING: from .drawBotPageDrawingTools import DrawBotPage + def _getmodulecontents(module, names=None): d = {} if names is None: @@ -1898,7 +1899,10 @@ def textOverflow( return self._dummyContext.clippedText(txt, box, align) def textBox( - self, txt: FormattedString | str, box: BoundingBox, align: str | None = None, + self, + txt: FormattedString | str, + box: BoundingBox, + align: str | None = None, ) -> str | FormattedString | None: """ Draw a text in a provided rectangle. From 659c70c10407822684d82b54688f4d47210f16a0 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Fri, 9 May 2025 10:00:47 +0200 Subject: [PATCH 11/14] add `None` --- drawBot/drawBotDrawingTools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 37f0191b..73835d40 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -505,7 +505,7 @@ def printImage(self, pdf=None) -> None: else: context.printImage(pdf) - def pdfImage(self) -> Quartz.PDFDocument: + def pdfImage(self) -> Quartz.PDFDocument | None: """ Return the image as a pdf document object. """ From 60f54304827bff8fb22ae4892ee8a37f7bab85d7 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Fri, 9 May 2025 10:10:42 +0200 Subject: [PATCH 12/14] Add `Literal` hints methods affected: - `lineCap` - `underline` - `strikethrough` - `writingDirection` - `text` - `textBox` - `textBoxBaselines` --- drawBot/drawBotDrawingTools.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 73835d40..631d28d8 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -5,7 +5,7 @@ import random from collections import namedtuple from contextlib import contextmanager -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import AppKit # type: ignore import CoreText # type: ignore @@ -1293,7 +1293,7 @@ def miterLimit(self, value: float) -> None: self._requiresNewFirstPage = True self._addInstruction("miterLimit", value) - def lineJoin(self, value: str): + def lineJoin(self, value: Literal["miter", "round", "bevel"]): """ Set a line join. @@ -1336,7 +1336,7 @@ def lineJoin(self, value: str): self._requiresNewFirstPage = True self._addInstruction("lineJoin", value) - def lineCap(self, value: str): + def lineCap(self, value: Literal["butt", "square", "round"]): """ Set a line cap. @@ -1565,7 +1565,7 @@ def baselineShift(self, value) -> None: self._dummyContext.baselineShift(value) self._addInstruction("baselineShift", value) - def underline(self, value: str | None) -> None: # FIXME could we assert the value in entrance? + def underline(self, value: Literal["single", "thick", "double"] | None) -> None: """ Set the underline value. Underline must be `single`, `thick`, `double` or `None`. @@ -1579,7 +1579,7 @@ def underline(self, value: str | None) -> None: # FIXME could we assert the val self._dummyContext.underline(value) self._addInstruction("underline", value) - def strikethrough(self, value: str | None) -> None: # FIXME could we assert the value in entrance? + def strikethrough(self, value: Literal["single", "thick", "double"] | None) -> None: """ Set the strikethrough value. Underline must be `single`, `thick`, `double` or `None`. @@ -1702,9 +1702,7 @@ def _checkLanguageHyphenation(self): if not CoreText.CFStringIsHyphenationAvailableForLocale(locale): warnings.warn(f"Language '{language}' has no hyphenation available.") - def writingDirection( - self, direction: str | None - ) -> None: # FIXME could we assert the value in entrance? or give a warning? + def writingDirection(self, direction: Literal["LTR", "RTL"] | None) -> None: """ Set the writing direction: `None`, `'LTR'` or `'RTL'`. @@ -1834,7 +1832,12 @@ def textProperties(self) -> dict[str, Any]: # drawing text - def text(self, txt: FormattedString | str, position: Point, align: str | None = None) -> None: + def text( + self, + txt: FormattedString | str, + position: Point, + align: Literal["left", "center", "right"] | None = None, + ) -> None: """ Draw a text at a provided position. @@ -1873,7 +1876,7 @@ def textOverflow( self, txt: FormattedString | str, box: BoundingBox | BezierPath, - align: str | None = None, + align: Literal["left", "center", "right", "justified"] | None = None, ) -> FormattedString | str | None: """ Returns the overflowed text without drawing the text. @@ -1902,7 +1905,7 @@ def textBox( self, txt: FormattedString | str, box: BoundingBox, - align: str | None = None, + align: Literal["left", "center", "right", "justified"] | None = None, ) -> str | FormattedString | None: """ Draw a text in a provided rectangle. @@ -2042,7 +2045,10 @@ def textBox( return self._dummyContext.clippedText(txt, box, align) def textBoxBaselines( - self, txt: FormattedString | str, box: BoundingBox, align: str | None = None + self, + txt: FormattedString | str, + box: BoundingBox, + align: Literal["left", "center", "right", "justified"] | None = None, ) -> list[tuple[float, float]]: """ Returns a list of `x, y` coordinates From 14f9c7ce3b3d2a5dea5567613fdbf2dbec4bccc4 Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Mon, 19 May 2025 15:55:21 +0200 Subject: [PATCH 13/14] improve hints --- drawBot/drawBotDrawingTools.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 631d28d8..1d83d0f9 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -395,9 +395,7 @@ def pages(self) -> tuple[DrawBotPage, ...]: break return tuple(DrawBotPage(instructionSet) for instructionSet in instructions) - def saveImage( - self, path: SomePath, *args: Any, **options: Any - ) -> list[AppKit.NSImage | PIL.ImageFile.ImageFile] | None: + def saveImage(self, path: SomePath, *args: Any, **options: Any): """ Save or export the canvas to a specified format. The `path` argument is a single destination path to save the current drawing actions. @@ -1721,11 +1719,7 @@ def writingDirection(self, direction: Literal["LTR", "RTL"] | None) -> None: self._dummyContext.writingDirection(direction) self._addInstruction("writingDirection", direction) - def openTypeFeatures( - self, *args: bool | None, **features: bool - ) -> dict[ - str, bool - ]: # FIXME I am sure we discussed this already, but why do we need args here? From the example is not evident... + def openTypeFeatures(self, *args: bool | None, **features: bool) -> dict[str, bool]: """ Enable OpenType features. @@ -2070,7 +2064,6 @@ def textBoxBaselines( origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) return [(x + o.x, y + o.y) for o in origins] - # FIXME align is not used def textBoxCharacterBounds( self, txt: FormattedString | str, box: BoundingBox, align: str | None = None ) -> list[tuple[BoundingBox | BezierPath, float, FormattedString | str]]: @@ -2087,7 +2080,7 @@ def textBoxCharacterBounds( CharactersBounds = namedtuple("CharactersBounds", ["bounds", "baselineOffset", "formattedSubString"]) - bounds = list() + bounds = list[tuple[BoundingBox | BezierPath, float, FormattedString | str]]() path, (x, y) = self._dummyContext._getPathForFrameSetter(box) attrString = self._dummyContext.attributedString(txt) setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) From 79400aca86d298e00de77c679ca2933ec1b58abd Mon Sep 17 00:00:00 2001 From: Roberto Arista Date: Mon, 19 May 2025 15:57:52 +0200 Subject: [PATCH 14/14] we can import PIL, so we can get this one back --- drawBot/drawBotDrawingTools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 1d83d0f9..c24130b2 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -9,6 +9,7 @@ import AppKit # type: ignore import CoreText # type: ignore +import PIL # type: ignore import Quartz # type: ignore from .aliases import ( @@ -395,7 +396,9 @@ def pages(self) -> tuple[DrawBotPage, ...]: break return tuple(DrawBotPage(instructionSet) for instructionSet in instructions) - def saveImage(self, path: SomePath, *args: Any, **options: Any): + def saveImage( + self, path: SomePath, *args: Any, **options: Any + ) -> list[AppKit.NSImage | PIL.ImageFile.ImageFile] | None: """ Save or export the canvas to a specified format. The `path` argument is a single destination path to save the current drawing actions.